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

Merge remote-tracking branch 'origin/develop' into dimension-door-changes

This commit is contained in:
Dydzio 2024-03-27 23:03:33 +01:00
commit 048fb1867d
698 changed files with 15306 additions and 12192 deletions

View File

@ -124,7 +124,7 @@ jobs:
# also, running it on multiple presets is redundant and slightly increases already long CI built times
if: ${{ startsWith(matrix.preset, 'linux-clang-test') }}
run: |
pip3 install json5 jstyleson
pip3 install jstyleson
python3 CI/linux-qt6/validate_json.py
- name: Dependencies
@ -134,7 +134,7 @@ jobs:
# ensure the ccache for each PR is separate so they don't interfere with each other
# fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found
- name: Ccache for PRs
- name: ccache for PRs
uses: hendrikmuhs/ccache-action@v1.2
if: ${{ github.event.number != '' }}
with:
@ -146,9 +146,9 @@ jobs:
max-size: "5G"
verbose: 2
- name: Ccache for vcmi/vcmi's develop branch
- name: ccache for everything but PRs
uses: hendrikmuhs/ccache-action@v1.2
if: ${{ github.event.number == '' && github.ref == 'refs/heads/develop' }}
if: ${{ (github.repository == 'vcmi/vcmi' && github.event.number == '' && github.ref == 'refs/heads/develop') || github.repository != 'vcmi/vcmi' }}
with:
key: ${{ matrix.preset }}-no-PR
restore-keys: |

View File

@ -62,16 +62,12 @@ void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int sid
int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
{
auto damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
bool wasComputedBefore = damageCache[attacker->unitId()].count(defender->unitId());
if(damage == 0)
{
if (!wasComputedBefore)
cacheDamage(attacker, defender, hb);
damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
}
return static_cast<int64_t>(damage);
return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
}
int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)

View File

@ -10,7 +10,6 @@
#pragma once
#include "../../lib/battle/CUnitState.h"
#include "../../CCallback.h"
#include "common.h"
#include "StackWithBonuses.h"
#define BATTLE_TRACE_LEVEL 0

View File

@ -49,7 +49,6 @@ CBattleAI::~CBattleAI()
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
{
setCbc(CB);
env = ENV;
cb = CB;
playerID = *CB->getPlayerID();
@ -121,7 +120,6 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
};
BattleAction result = BattleAction::makeDefend(stack);
setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
auto start = std::chrono::high_resolution_clock::now();

View File

@ -270,7 +270,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
{
float score = evaluateExchange(ap, 0, targets, damageCache, hb);
if(score > result.score || (score == result.score && result.wait))
if(score > result.score || (vstd::isAlmostEqual(score, result.score) && result.wait))
{
result.score = score;
result.bestAttack = ap;

View File

@ -2,7 +2,6 @@ set(battleAI_SRCS
AttackPossibility.cpp
BattleAI.cpp
BattleEvaluator.cpp
common.cpp
EnemyInfo.cpp
PossibleSpellcast.cpp
PotentialTargets.cpp
@ -17,7 +16,6 @@ set(battleAI_HEADERS
AttackPossibility.h
BattleAI.h
BattleEvaluator.h
common.h
EnemyInfo.h
PotentialTargets.h
PossibleSpellcast.h
@ -26,12 +24,12 @@ set(battleAI_HEADERS
BattleExchangeVariant.h
)
if(NOT ENABLE_STATIC_AI_LIBS)
if(NOT ENABLE_STATIC_LIBS)
list(APPEND battleAI_SRCS main.cpp StdInc.cpp)
endif()
assign_source_group(${battleAI_SRCS} ${battleAI_HEADERS})
if(ENABLE_STATIC_AI_LIBS)
if(ENABLE_STATIC_LIBS)
add_library(BattleAI STATIC ${battleAI_SRCS} ${battleAI_HEADERS})
else()
add_library(BattleAI SHARED ${battleAI_SRCS} ${battleAI_HEADERS})
@ -39,7 +37,7 @@ else()
endif()
target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET} TBB::tbb)
target_link_libraries(BattleAI PRIVATE vcmi TBB::tbb)
vcmi_set_output_dir(BattleAI "AI")
enable_pch(BattleAI)

View File

@ -1,23 +0,0 @@
/*
* common.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 "common.h"
std::shared_ptr<CBattleCallback> cbc;
void setCbc(std::shared_ptr<CBattleCallback> cb)
{
cbc = cb;
}
std::shared_ptr<CBattleCallback> getCbc()
{
return cbc;
}

View File

@ -1,26 +0,0 @@
/*
* common.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
class CBattleCallback;
template<typename Key, typename Val, typename Val2>
const Val getValOr(const std::map<Key, Val> &Map, const Key &key, const Val2 defaultValue)
{
//returning references here won't work: defaultValue must be converted into Val, creating temporary
auto i = Map.find(key);
if(i != Map.end())
return i->second;
else
return defaultValue;
}
void setCbc(std::shared_ptr<CBattleCallback> cb);
std::shared_ptr<CBattleCallback> getCbc();

View File

@ -15,7 +15,7 @@
#define strcpy_s(a, b, c) strncpy(a, c, b)
#endif
static const char *g_cszAiName = "Battle AI";
static const char * const g_cszAiName = "Battle AI";
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{

View File

@ -19,8 +19,8 @@ class CEmptyAI : public CGlobalAI
std::shared_ptr<CCallback> cb;
public:
virtual void saveGame(BinarySerializer & h) override;
virtual void loadGame(BinaryDeserializer & h) override;
void saveGame(BinarySerializer & h) override;
void loadGame(BinaryDeserializer & h) override;
void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
void yourTurn(QueryID queryID) override;

View File

@ -8,12 +8,12 @@ set(emptyAI_HEADERS
CEmptyAI.h
)
if(NOT ENABLE_STATIC_AI_LIBS)
if(NOT ENABLE_STATIC_LIBS)
list(APPEND emptyAI_SRCS main.cpp StdInc.cpp)
endif()
assign_source_group(${emptyAI_SRCS} ${emptyAI_HEADERS})
if(ENABLE_STATIC_AI_LIBS)
if(ENABLE_STATIC_LIBS)
add_library(EmptyAI STATIC ${emptyAI_SRCS} ${emptyAI_HEADERS})
else()
add_library(EmptyAI SHARED ${emptyAI_SRCS} ${emptyAI_HEADERS})
@ -21,7 +21,7 @@ else()
endif()
target_include_directories(EmptyAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(EmptyAI PRIVATE ${VCMI_LIB_TARGET})
target_link_libraries(EmptyAI PRIVATE vcmi)
vcmi_set_output_dir(EmptyAI "AI")
enable_pch(EmptyAI)

View File

@ -11,7 +11,6 @@
#include "CEmptyAI.h"
std::set<CGlobalAI*> ais;
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{
return AI_INTERFACE_VER;

View File

@ -374,6 +374,11 @@ void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor &
nullkiller->memory->removeFromMemory(obj);
if(nullkiller->baseGraph && nullkiller->settings->isObjectGraphAllowed())
{
nullkiller->baseGraph->removeObject(obj);
}
if(obj->ID == Obj::HERO && obj->tempOwner == playerID)
{
lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion
@ -632,7 +637,8 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
auto topObj = objects.front()->id == hero->id ? objects.back() : objects.front();
auto objType = topObj->ID; // top object should be our hero
auto goalObjectID = nullkiller->getTargetObject();
auto ratio = (float)nullkiller->dangerEvaluator->evaluateDanger(target, hero.get()) / (float)hero->getTotalStrength();
auto danger = nullkiller->dangerEvaluator->evaluateDanger(target, hero.get());
auto ratio = static_cast<float>(danger) / hero->getTotalStrength();
answer = topObj->id == goalObjectID; // no if we do not aim to visit this object
logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name, ratio);
@ -648,7 +654,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
}
else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
{
bool dangerUnknown = ratio == 0;
bool dangerUnknown = danger == 0;
bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
answer = !dangerUnknown && !dangerTooHigh;
@ -676,9 +682,9 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
&& components.size() == 2
&& components.front().type == ComponentType::RESOURCE
&& (nullkiller->heroManager->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;
}
}
@ -1406,7 +1412,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
int accquiredResources = 0;
if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false))
{
if(const IMarket * m = IMarket::castFrom(obj, false))
if(const auto * m = dynamic_cast<const IMarket*>(obj))
{
auto freeRes = cb->getResourceAmount(); //trade only resources which are not reserved
for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++)

View File

@ -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:

View File

@ -185,8 +185,8 @@ void foreach_tile_pos(const Func & foo)
}
}
template<class Func>
void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retrieval of thread-specific pointer
template<class Func, class TCallback>
void foreach_tile_pos(TCallback * cbp, const Func & foo) // avoid costly retrieval of thread-specific pointer
{
int3 mapSize = cbp->getMapSize();
for(int z = 0; z < mapSize.z; z++)

View File

@ -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");

View File

@ -96,6 +96,7 @@ public:
const std::vector<TownDevelopmentInfo> & 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:

View File

@ -16,7 +16,7 @@
namespace NKAI
{
HitMapInfo HitMapInfo::NoThreat;
const HitMapInfo HitMapInfo::NoThreat;
double HitMapInfo::value() const
{
@ -75,8 +75,7 @@ void DangerHitMapAnalyzer::updateHitMap()
PathfinderSettings ps;
ps.mainTurnDistanceLimit = 10;
ps.scoutTurnDistanceLimit = 10;
ps.scoutTurnDistanceLimit = ps.mainTurnDistanceLimit = ai->settings->getMainHeroTurnDistanceLimit();
ps.useHeroChain = false;
ai->pathfinder->updatePaths(pair.second, ps);
@ -158,15 +157,13 @@ void DangerHitMapAnalyzer::calculateTileOwners()
if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z)
hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
std::map<const CGHeroInstance *, HeroRole> townHeroes;
std::vector<std::unique_ptr<CGHeroInstance>> temporaryHeroes;
std::map<const CGHeroInstance *, const CGTownInstance *> heroTownMap;
PathfinderSettings pathfinderSettings;
pathfinderSettings.mainTurnDistanceLimit = 5;
std::map<const CGHeroInstance *, HeroRole> townHeroes;
auto addTownHero = [&](const CGTownInstance * town)
{
auto townHero = new CGHeroInstance(town->cb);
auto townHero = temporaryHeroes.emplace_back(std::make_unique<CGHeroInstance>(town->cb)).get();
CRandomGenerator rng;
auto visitablePos = town->visitablePos();
@ -192,7 +189,10 @@ void DangerHitMapAnalyzer::calculateTileOwners()
addTownHero(town);
}
ai->pathfinder->updatePaths(townHeroes, PathfinderSettings());
PathfinderSettings ps;
ps.mainTurnDistanceLimit = ps.scoutTurnDistanceLimit = ai->settings->getMainHeroTurnDistanceLimit();
ai->pathfinder->updatePaths(townHeroes, ps);
pforeachTilePos(mapSize, [&](const int3 & pos)
{
@ -226,7 +226,7 @@ void DangerHitMapAnalyzer::calculateTileOwners()
}
}
if(ourDistance == enemyDistance)
if(vstd::isAlmostEqual(ourDistance, enemyDistance))
{
hitMap[pos.x][pos.y][pos.z].closestTown = nullptr;
}
@ -285,8 +285,6 @@ const HitMapNode & DangerHitMapAnalyzer::getTileThreat(const int3 & tile) const
return hitMap[tile.x][tile.y][tile.z];
}
const std::set<const CGObjectInstance *> empty = {};
std::set<const CGObjectInstance *> DangerHitMapAnalyzer::getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const
{
std::set<const CGObjectInstance *> result;

View File

@ -18,7 +18,7 @@ struct AIPath;
struct HitMapInfo
{
static HitMapInfo NoThreat;
static const HitMapInfo NoThreat;
uint64_t danger;
uint8_t turn;

View File

@ -17,7 +17,7 @@
namespace NKAI
{
SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluator(
const SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluator(
{
std::make_shared<SecondarySkillScoreMap>(
std::map<SecondarySkill, float>
@ -46,7 +46,7 @@ SecondarySkillEvaluator HeroManager::wariorSkillsScores = SecondarySkillEvaluato
std::make_shared<AtLeastOneMagicRule>()
});
SecondarySkillEvaluator HeroManager::scountSkillsScores = SecondarySkillEvaluator(
const SecondarySkillEvaluator HeroManager::scountSkillsScores = SecondarySkillEvaluator(
{
std::make_shared<SecondarySkillScoreMap>(
std::map<SecondarySkill, float>
@ -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);
}
@ -332,7 +333,7 @@ void WisdomRule::evaluateScore(const CGHeroInstance * hero, SecondarySkill skill
score += 1.5;
}
std::vector<SecondarySkill> AtLeastOneMagicRule::magicSchools = {
const std::vector<SecondarySkill> AtLeastOneMagicRule::magicSchools = {
SecondarySkill::AIR_MAGIC,
SecondarySkill::EARTH_MAGIC,
SecondarySkill::FIRE_MAGIC,

View File

@ -58,8 +58,8 @@ public:
class DLL_EXPORT HeroManager : public IHeroManager
{
private:
static SecondarySkillEvaluator wariorSkillsScores;
static SecondarySkillEvaluator scountSkillsScores;
static const SecondarySkillEvaluator wariorSkillsScores;
static const SecondarySkillEvaluator scountSkillsScores;
CCallback * cb; //this is enough, but we downcast from CCallback
const Nullkiller * ai;
@ -114,7 +114,7 @@ public:
class AtLeastOneMagicRule : public ISecondarySkillRule
{
private:
static std::vector<SecondarySkill> magicSchools;
static const std::vector<SecondarySkill> magicSchools;
public:
void evaluateScore(const CGHeroInstance * hero, SecondarySkill skill, float & score) const override;

View File

@ -189,15 +189,7 @@ bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const
return true; //all of the following is met
}
void ObjectClusterizer::clusterize()
{
auto start = std::chrono::high_resolution_clock::now();
nearObjects.reset();
farObjects.reset();
blockedObjects.clear();
Obj ignoreObjects[] = {
Obj ObjectClusterizer::IgnoredObjectTypes[] = {
Obj::BOAT,
Obj::EYE_OF_MAGI,
Obj::MONOLITH_ONE_WAY_ENTRANCE,
@ -216,7 +208,15 @@ void ObjectClusterizer::clusterize()
Obj::REDWOOD_OBSERVATORY,
Obj::CARTOGRAPHER,
Obj::PILLAR_OF_FIRE
};
};
void ObjectClusterizer::clusterize()
{
auto start = std::chrono::high_resolution_clock::now();
nearObjects.reset();
farObjects.reset();
blockedObjects.clear();
logAi->debug("Begin object clusterization");
@ -224,131 +224,20 @@ void ObjectClusterizer::clusterize()
ai->memory->visitableObjs.begin(),
ai->memory->visitableObjs.end());
parallel_for(blocked_range<size_t>(0, objs.size()), [&](const blocked_range<size_t> & r)
{
#if NKAI_TRACE_LEVEL == 0
parallel_for(blocked_range<size_t>(0, objs.size()), [&](const blocked_range<size_t> & r) {
#else
blocked_range<size_t> r(0, objs.size());
#endif
auto priorityEvaluator = ai->priorityEvaluators->acquire();
for(int i = r.begin(); i != r.end(); i++)
{
auto obj = objs[i];
if(!shouldVisitObject(obj))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
#endif
continue;
}
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
#endif
auto paths = ai->pathfinder->getPathInfo(obj->visitablePos());
if(paths.empty())
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("No paths found.");
#endif
continue;
}
std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool
{
return p1.movementCost() < p2.movementCost();
});
if(vstd::contains(ignoreObjects, obj->ID))
{
farObjects.addObject(obj, paths.front(), 0);
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString());
#endif
continue;
}
std::set<const CGHeroInstance *> heroesProcessed;
for(auto & path : paths)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Checking path %s", path.toString());
#endif
if(!shouldVisit(ai, path.targetHero, obj))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Hero %s does not need to visit %s", path.targetHero->getObjectName(), obj->getObjectName());
#endif
continue;
}
if(path.nodes.size() > 1)
{
auto blocker = getBlocker(path);
if(blocker)
{
if(vstd::contains(heroesProcessed, path.targetHero))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Hero %s is already processed.", path.targetHero->getObjectName());
#endif
continue;
}
heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
if(priority < MIN_PRIORITY)
continue;
ClusterMap::accessor cluster;
blockedObjects.insert(
cluster,
ClusterMap::value_type(blocker, std::make_shared<ObjectCluster>(blocker)));
cluster->second->addObject(obj, path, priority);
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path added to cluster %s%s", blocker->getObjectName(), blocker->visitablePos().toString());
#endif
continue;
}
}
heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
if(priority < MIN_PRIORITY)
continue;
bool interestingObject = path.turn() <= 2 || priority > 0.5f;
if(interestingObject)
{
nearObjects.addObject(obj, path, priority);
}
else
{
farObjects.addObject(obj, path, priority);
}
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path %s added to %s objects. Turn: %d, priority: %f",
path.toString(),
interestingObject ? "near" : "far",
path.turn(),
priority);
#endif
}
clusterizeObject(objs[i], priorityEvaluator.get());
}
#if NKAI_TRACE_LEVEL == 0
});
#endif
logAi->trace("Near objects count: %i", nearObjects.objects.size());
logAi->trace("Far objects count: %i", farObjects.objects.size());
@ -368,4 +257,123 @@ void ObjectClusterizer::clusterize()
logAi->trace("Clusterization complete in %ld", timeElapsed(start));
}
void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator)
{
if(!shouldVisitObject(obj))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
#endif
return;
}
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
#endif
auto paths = ai->pathfinder->getPathInfo(obj->visitablePos(), true);
if(paths.empty())
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("No paths found.");
#endif
return;
}
std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool
{
return p1.movementCost() < p2.movementCost();
});
if(vstd::contains(IgnoredObjectTypes, obj->ID))
{
farObjects.addObject(obj, paths.front(), 0);
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString());
#endif
return;
}
std::set<const CGHeroInstance *> heroesProcessed;
for(auto & path : paths)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Checking path %s", path.toString());
#endif
if(!shouldVisit(ai, path.targetHero, obj))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Hero %s does not need to visit %s", path.targetHero->getObjectName(), obj->getObjectName());
#endif
continue;
}
if(path.nodes.size() > 1)
{
auto blocker = getBlocker(path);
if(blocker)
{
if(vstd::contains(heroesProcessed, path.targetHero))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Hero %s is already processed.", path.targetHero->getObjectName());
#endif
continue;
}
heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
if(priority < MIN_PRIORITY)
continue;
ClusterMap::accessor cluster;
blockedObjects.insert(
cluster,
ClusterMap::value_type(blocker, std::make_shared<ObjectCluster>(blocker)));
cluster->second->addObject(obj, path, priority);
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path added to cluster %s%s", blocker->getObjectName(), blocker->visitablePos().toString());
#endif
continue;
}
}
heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
if(priority < MIN_PRIORITY)
continue;
bool interestingObject = path.turn() <= 2 || priority > 0.5f;
if(interestingObject)
{
nearObjects.addObject(obj, path, priority);
}
else
{
farObjects.addObject(obj, path, priority);
}
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path %s added to %s objects. Turn: %d, priority: %f",
path.toString(),
interestingObject ? "near" : "far",
path.turn(),
priority);
#endif
}
}
}

View File

@ -49,9 +49,13 @@ public:
using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
class PriorityEvaluator;
class ObjectClusterizer
{
private:
static Obj IgnoredObjectTypes[];
ObjectCluster nearObjects;
ObjectCluster farObjects;
ClusterMap blockedObjects;
@ -68,6 +72,7 @@ public:
private:
bool shouldVisitObject(const CGObjectInstance * obj) const;
void clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator);
};
}

View File

@ -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)
{

View File

@ -25,9 +25,9 @@ namespace Goals
{
}
virtual Goals::TGoalVec decompose() const override;
virtual std::string toString() const override;
virtual bool operator==(const BuildingBehavior & other) const override
Goals::TGoalVec decompose() const override;
std::string toString() const override;
bool operator==(const BuildingBehavior & other) const override
{
return true;
}

View File

@ -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;
}

View File

@ -24,9 +24,9 @@ namespace Goals
{
}
virtual Goals::TGoalVec decompose() const override;
virtual std::string toString() const override;
virtual bool operator==(const BuyArmyBehavior & other) const override
Goals::TGoalVec decompose() const override;
std::string toString() const override;
bool operator==(const BuyArmyBehavior & other) const override
{
return true;
}

View File

@ -73,13 +73,25 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
}
if(objToVisit && !shouldVisit(ai->nullkiller.get(), path.targetHero, objToVisit))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Hero %s should not visit obj %s", path.targetHero->getNameTranslated(), objToVisit->getObjectName());
#endif
continue;
}
auto hero = path.targetHero;
auto danger = path.getTotalDanger();
if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && path.exchangeCount > 1)
if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
&& (path.getTotalDanger() == 0 || path.turn() > 0)
&& path.exchangeCount > 1)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Hero %s is SCOUT, chain used and no danger", path.targetHero->getNameTranslated());
#endif
continue;
}
auto firstBlockedAction = path.getFirstBlockedAction();
if(firstBlockedAction)
@ -178,8 +190,11 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
#endif
const int3 pos = objToVisit->visitablePos();
bool useObjectGraph = ai->nullkiller->settings->isObjectGraphAllowed()
&& ai->nullkiller->getScanDepth() != ScanDepth::SMALL;
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos, useObjectGraph);
auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
std::shared_ptr<ExecuteHeroChain> closestWay;

View File

@ -48,8 +48,8 @@ namespace Goals
specificObjects = true;
}
virtual Goals::TGoalVec decompose() const override;
virtual std::string toString() const override;
Goals::TGoalVec decompose() const override;
std::string toString() const override;
CaptureObjectsBehavior & ofType(int type)
{
@ -65,7 +65,7 @@ namespace Goals
return *this;
}
virtual bool operator==(const CaptureObjectsBehavior & other) const override;
bool operator==(const CaptureObjectsBehavior & other) const override;
static Goals::TGoalVec getVisitGoals(const std::vector<AIPath> & paths, const CGObjectInstance * objToVisit = nullptr);

View File

@ -42,7 +42,7 @@ Goals::TGoalVec ClusterBehavior::decompose() const
Goals::TGoalVec ClusterBehavior::decomposeCluster(std::shared_ptr<ObjectCluster> cluster) const
{
auto center = cluster->calculateCenter();
auto paths = ai->nullkiller->pathfinder->getPathInfo(center->visitablePos());
auto paths = ai->nullkiller->pathfinder->getPathInfo(center->visitablePos(), ai->nullkiller->settings->isObjectGraphAllowed());
auto blockerPos = cluster->blocker->visitablePos();
std::vector<AIPath> blockerPaths;

View File

@ -28,10 +28,10 @@ namespace Goals
{
}
virtual TGoalVec decompose() const override;
virtual std::string toString() const override;
TGoalVec decompose() const override;
std::string toString() const override;
virtual bool operator==(const ClusterBehavior & other) const override
bool operator==(const ClusterBehavior & other) const override
{
return true;
}

View File

@ -443,6 +443,10 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
heroToDismiss = town->garrisonHero.get();
}
}
// avoid dismissing one weak hero in order to recruit another.
if(heroToDismiss && heroToDismiss->getArmyStrength() + 500 > hero->getArmyStrength())
continue;
}
else if(ai->nullkiller->heroManager->heroCapReached())
{

View File

@ -29,10 +29,10 @@ namespace Goals
{
}
virtual Goals::TGoalVec decompose() const override;
virtual std::string toString() const override;
Goals::TGoalVec decompose() const override;
std::string toString() const override;
virtual bool operator==(const DefenceBehavior & other) const override
bool operator==(const DefenceBehavior & other) const override
{
return true;
}

View File

@ -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;

View File

@ -25,10 +25,10 @@ namespace Goals
{
}
virtual TGoalVec decompose() const override;
virtual std::string toString() const override;
TGoalVec decompose() const override;
std::string toString() const override;
virtual bool operator==(const GatherArmyBehavior & other) const override
bool operator==(const GatherArmyBehavior & other) const override
{
return true;
}

View File

@ -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)));
}

View File

@ -25,10 +25,10 @@ namespace Goals
{
}
virtual TGoalVec decompose() const override;
virtual std::string toString() const override;
TGoalVec decompose() const override;
std::string toString() const override;
virtual bool operator==(const RecruitHeroBehavior & other) const override
bool operator==(const RecruitHeroBehavior & other) const override
{
return true;
}

View File

@ -25,10 +25,10 @@ namespace Goals
{
}
virtual TGoalVec decompose() const override;
virtual std::string toString() const override;
TGoalVec decompose() const override;
std::string toString() const override;
virtual bool operator==(const StartupBehavior & other) const override
bool operator==(const StartupBehavior & other) const override
{
return true;
}

View File

@ -25,10 +25,10 @@ namespace Goals
{
}
virtual TGoalVec decompose() const override;
virtual std::string toString() const override;
TGoalVec decompose() const override;
std::string toString() const override;
virtual bool operator==(const StayAtTownBehavior & other) const override
bool operator==(const StayAtTownBehavior & other) const override
{
return true;
}

View File

@ -14,9 +14,11 @@ set(Nullkiller_SRCS
Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
Pathfinding/Rules/AIMovementToDestinationRule.cpp
Pathfinding/Rules/AIPreviousNodeRule.cpp
Pathfinding/ObjectGraph.cpp
AIUtility.cpp
Analyzers/ArmyManager.cpp
Analyzers/HeroManager.cpp
Engine/Settings.cpp
Engine/FuzzyEngines.cpp
Engine/FuzzyHelper.cpp
Engine/AIMemory.cpp
@ -77,9 +79,11 @@ set(Nullkiller_HEADERS
Pathfinding/Rules/AIMovementAfterDestinationRule.h
Pathfinding/Rules/AIMovementToDestinationRule.h
Pathfinding/Rules/AIPreviousNodeRule.h
Pathfinding/ObjectGraph.h
AIUtility.h
Analyzers/ArmyManager.h
Analyzers/HeroManager.h
Engine/Settings.h
Engine/FuzzyEngines.h
Engine/FuzzyHelper.h
Engine/AIMemory.h
@ -125,12 +129,12 @@ set(Nullkiller_HEADERS
AIGateway.h
)
if(NOT ENABLE_STATIC_AI_LIBS)
if(NOT ENABLE_STATIC_LIBS)
list(APPEND Nullkiller_SRCS main.cpp StdInc.cpp)
endif()
assign_source_group(${Nullkiller_SRCS} ${Nullkiller_HEADERS})
if(ENABLE_STATIC_AI_LIBS)
if(ENABLE_STATIC_LIBS)
add_library(Nullkiller STATIC ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
else()
add_library(Nullkiller SHARED ${Nullkiller_SRCS} ${Nullkiller_HEADERS})
@ -138,7 +142,7 @@ else()
endif()
target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(Nullkiller PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite TBB::tbb)
target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite TBB::tbb)
vcmi_set_output_dir(Nullkiller "AI")
enable_pch(Nullkiller)

View File

@ -27,15 +27,14 @@ namespace NKAI
using namespace Goals;
#if NKAI_TRACE_LEVEL >= 1
#define MAXPASS 1000000
#else
#define MAXPASS 30
#endif
// while we play vcmieagles graph can be shared
std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
Nullkiller::Nullkiller()
:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
{
memory.reset(new AIMemory());
memory = std::make_unique<AIMemory>();
settings = std::make_unique<Settings>();
}
void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
@ -43,6 +42,8 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
this->cb = cb;
this->playerID = playerID;
baseGraph.reset();
priorityEvaluator.reset(new PriorityEvaluator(this));
priorityEvaluators.reset(
new SharedPool<PriorityEvaluator>(
@ -123,6 +124,12 @@ void Nullkiller::resetAiState()
lockedHeroes.clear();
dangerHitMap->reset();
useHeroChain = true;
if(!baseGraph && ai->nullkiller->settings->isObjectGraphAllowed())
{
baseGraph = std::make_unique<ObjectGraph>();
baseGraph->updateGraph(this);
}
}
void Nullkiller::updateAiState(int pass, bool fast)
@ -163,21 +170,27 @@ void Nullkiller::updateAiState(int pass, bool fast)
PathfinderSettings cfg;
cfg.useHeroChain = useHeroChain;
cfg.allowBypassObjects = true;
if(scanDepth == ScanDepth::SMALL)
if(scanDepth == ScanDepth::SMALL || settings->isObjectGraphAllowed())
{
cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
cfg.mainTurnDistanceLimit = settings->getMainHeroTurnDistanceLimit();
}
if(scanDepth != ScanDepth::ALL_FULL)
if(scanDepth != ScanDepth::ALL_FULL || settings->isObjectGraphAllowed())
{
cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
cfg.scoutTurnDistanceLimit =settings->getScoutHeroTurnDistanceLimit();
}
boost::this_thread::interruption_point();
pathfinder->updatePaths(activeHeroes, cfg);
if(settings->isObjectGraphAllowed())
{
pathfinder->updateGraphs(activeHeroes);
}
boost::this_thread::interruption_point();
objectClusterizer->clusterize();
@ -235,13 +248,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),
@ -290,7 +303,8 @@ void Nullkiller::makeTurn()
// TODO: better to check turn distance here instead of priority
if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
&& scanDepth == ScanDepth::MAIN_FULL)
&& scanDepth == ScanDepth::MAIN_FULL
&& !settings->isObjectGraphAllowed())
{
useHeroChain = false;
scanDepth = ScanDepth::SMALL;
@ -303,22 +317,25 @@ void Nullkiller::makeTurn()
if(bestTask->priority < MIN_PRIORITY)
{
auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
{
return h->movementPointsRemaining() > 100;
});
if(hasMp && scanDepth != ScanDepth::ALL_FULL)
if(!settings->isObjectGraphAllowed())
{
logAi->trace(
"Goal %s has too low priority %f so increasing scan depth to full.",
taskDescription,
bestTask->priority);
auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
{
return h->movementPointsRemaining() > 100;
});
scanDepth = ScanDepth::ALL_FULL;
useHeroChain = false;
continue;
if(hasMp && scanDepth != ScanDepth::ALL_FULL)
{
logAi->trace(
"Goal %s has too low priority %f so increasing scan depth to full.",
taskDescription,
bestTask->priority);
scanDepth = ScanDepth::ALL_FULL;
useHeroChain = false;
continue;
}
}
logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", taskDescription);
@ -328,9 +345,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);
}
}
}

View File

@ -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;
@ -59,6 +59,8 @@ private:
bool useHeroChain;
public:
static std::unique_ptr<ObjectGraph> baseGraph;
std::unique_ptr<DangerHitMapAnalyzer> dangerHitMap;
std::unique_ptr<BuildAnalyzer> buildAnalyzer;
std::unique_ptr<ObjectClusterizer> objectClusterizer;
@ -71,6 +73,7 @@ public:
std::unique_ptr<FuzzyHelper> dangerEvaluator;
std::unique_ptr<DeepDecomposer> decomposer;
std::unique_ptr<ArmyFormation> armyFormation;
std::unique_ptr<Settings> settings;
PlayerColor playerID;
std::shared_ptr<CCallback> cb;
std::mutex aiStateMutex;

View File

@ -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");
@ -702,7 +702,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
class HeroExchangeEvaluator : public IEvaluationContextBuilder
{
public:
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::HERO_EXCHANGE)
return;
@ -719,7 +719,7 @@ public:
class ArmyUpgradeEvaluator : public IEvaluationContextBuilder
{
public:
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::ARMY_UPGRADE)
return;
@ -736,7 +736,7 @@ public:
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
{
public:
virtual 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)
return;
@ -771,7 +771,7 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
class DefendTownEvaluator : public IEvaluationContextBuilder
{
public:
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::DEFEND_TOWN)
return;
@ -821,7 +821,7 @@ private:
public:
ExecuteHeroChainEvaluationContextBuilder(const Nullkiller * ai) : ai(ai) {}
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::EXECUTE_HERO_CHAIN)
return;
@ -879,7 +879,7 @@ class ClusterEvaluationContextBuilder : public IEvaluationContextBuilder
public:
ClusterEvaluationContextBuilder(const Nullkiller * ai) {}
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::UNLOCK_CLUSTER)
return;
@ -926,7 +926,7 @@ public:
class ExchangeSwapTownHeroesContextBuilder : public IEvaluationContextBuilder
{
public:
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::EXCHANGE_SWAP_TOWN_HEROES)
return;
@ -954,7 +954,7 @@ private:
public:
DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {}
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::DISMISS_HERO)
return;
@ -974,7 +974,7 @@ public:
class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
{
public:
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::BUILD_STRUCTURE)
return;

View File

@ -0,0 +1,84 @@
/*
* 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 <limits>
#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),
allowObjectGraph(false)
{
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();
}
if(!node.Struct()["allowObjectGraph"].isNull())
{
allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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;
bool allowObjectGraph;
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; }
bool isObjectGraphAllowed() const { return allowObjectGraph; }
private:
void loadFromMod(const std::string & modName, const ResourcePath & resource);
};
}

View File

@ -180,11 +180,7 @@ public:
{
}
virtual ~cannotFulfillGoalException() throw ()
{
};
const char * what() const throw () override
const char * what() const noexcept override
{
return msg.c_str();
}
@ -203,11 +199,7 @@ public:
msg = goal->toString();
}
virtual ~goalFulfilledException() throw ()
{
};
const char * what() const throw () override
const char * what() const noexcept override
{
return msg.c_str();
}

View File

@ -39,12 +39,18 @@ void AdventureSpellCast::accept(AIGateway * ai)
if(hero->mana < hero->getSpellCost(spell))
throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->getNameTranslated());
if(spellID == SpellID::TOWN_PORTAL && town && town->visitingHero)
throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
if(town && spellID == SpellID::TOWN_PORTAL)
{
ai->selectedObject = town->id;
if(town->visitingHero && town->tempOwner == ai->playerID && !town->getUpperArmy()->stacksCount())
{
ai->myCb->swapGarrisonHero(town);
}
if(town->visitingHero)
throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
}
auto wait = cb->waitTillRealize;

View File

@ -35,7 +35,7 @@ namespace Goals
void accept(AIGateway * ai) override;
std::string toString() const override;
virtual bool operator==(const AdventureSpellCast & other) const override;
bool operator==(const AdventureSpellCast & other) const override;
};
}

View File

@ -32,7 +32,7 @@ namespace Goals
TSubgoal whatToDoToAchieve() override;
bool fulfillsMe(TSubgoal goal) override;
virtual bool operator==(const Build & other) const override
bool operator==(const Build & other) const override
{
return true;
}

View File

@ -29,7 +29,7 @@ namespace Goals
void accept(AIGateway * ai) override;
std::string toString() const override;
virtual bool operator==(const BuildBoat & other) const override;
bool operator==(const BuildBoat & other) const override;
};
}

View File

@ -39,8 +39,8 @@ namespace Goals
}
BuildThis(BuildingID Bid, const CGTownInstance * tid);
virtual bool operator==(const BuildThis & other) const override;
virtual std::string toString() const override;
bool operator==(const BuildThis & other) const override;
std::string toString() const override;
void accept(AIGateway * ai) override;
};
}

View File

@ -36,11 +36,11 @@ namespace Goals
priority = 3;//TODO: evaluate?
}
virtual bool operator==(const BuyArmy & other) const override;
bool operator==(const BuyArmy & other) const override;
virtual std::string toString() const override;
std::string toString() const override;
virtual void accept(AIGateway * ai) override;
void accept(AIGateway * ai) override;
};
}

View File

@ -44,7 +44,7 @@ namespace Goals
//h & value & resID & objid & aid & tile & hero & town & bid;
}
virtual bool operator==(const AbstractGoal & g) const override
bool operator==(const AbstractGoal & g) const override
{
if(goalType != g.goalType)
return false;
@ -54,7 +54,7 @@ namespace Goals
virtual bool operator==(const T & other) const = 0;
virtual TGoalVec decompose() const override
TGoalVec decompose() const override
{
TSubgoal single = decomposeSingle();
@ -90,11 +90,11 @@ namespace Goals
return *((T *)this);
}
virtual bool isElementar() const override { return true; }
bool isElementar() const override { return true; }
virtual HeroPtr getHero() const override { return AbstractGoal::hero; }
HeroPtr getHero() const override { return AbstractGoal::hero; }
virtual int getHeroExchangeCount() const override { return 0; }
int getHeroExchangeCount() const override { return 0; }
};
}

View File

@ -34,11 +34,11 @@ namespace Goals
name = obj->getObjectName();
}
virtual bool operator==(const CaptureObject & other) const override;
virtual Goals::TGoalVec decompose() const override;
virtual std::string toString() const override;
virtual bool hasHash() const override { return true; }
virtual uint64_t getHash() const override;
bool operator==(const CaptureObject & other) const override;
Goals::TGoalVec decompose() const override;
std::string toString() const override;
bool hasHash() const override { return true; }
uint64_t getHash() const override;
};
}

View File

@ -29,12 +29,12 @@ namespace Goals
{
}
virtual Goals::TGoalVec decompose() const override;
virtual std::string toString() const override;
virtual bool hasHash() const override { return true; }
virtual uint64_t getHash() const override;
Goals::TGoalVec decompose() const override;
std::string toString() const override;
bool hasHash() const override { return true; }
uint64_t getHash() const override;
virtual bool operator==(const CompleteQuest & other) const override;
bool operator==(const CompleteQuest & other) const override;
private:
TGoalVec tryCompleteQuest() const;

View File

@ -26,15 +26,15 @@ namespace Goals
{
}
virtual bool operator==(const Composition & other) const override;
virtual std::string toString() const override;
bool operator==(const Composition & other) const override;
std::string toString() const override;
void accept(AIGateway * ai) override;
Composition & addNext(const AbstractGoal & goal);
Composition & addNext(TSubgoal goal);
Composition & addNextSequence(const TGoalVec & taskSequence);
virtual TGoalVec decompose() const override;
virtual bool isElementar() const override;
virtual int getHeroExchangeCount() const override;
TGoalVec decompose() const override;
bool isElementar() const override;
int getHeroExchangeCount() const override;
};
}

View File

@ -33,7 +33,7 @@ namespace Goals
{
tile = Tile;
}
virtual bool operator==(const DigAtTile & other) const override;
bool operator==(const DigAtTile & other) const override;
private:
//TSubgoal decomposeSingle() const override;

View File

@ -26,7 +26,7 @@ namespace Goals
void accept(AIGateway * ai) override;
std::string toString() const override;
virtual bool operator==(const DismissHero & other) const override;
bool operator==(const DismissHero & other) const override;
};
}

View File

@ -31,7 +31,7 @@ namespace Goals
void accept(AIGateway * ai) override;
std::string toString() const override;
virtual bool operator==(const ExchangeSwapTownHeroes & other) const override;
bool operator==(const ExchangeSwapTownHeroes & other) const override;
const CGHeroInstance * getGarrisonHero() const { return garrisonHero; }
HeroLockedReason getLockingReason() const { return lockingReason; }

View File

@ -30,10 +30,10 @@ namespace Goals
void accept(AIGateway * ai) override;
std::string toString() const override;
virtual bool operator==(const ExecuteHeroChain & other) const override;
bool operator==(const ExecuteHeroChain & other) const override;
const AIPath & getPath() const { return chainPath; }
virtual int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
int getHeroExchangeCount() const override { return chainPath.exchangeCount; }
private:
bool moveHeroToTile(const CGHeroInstance * hero, const int3 & tile);

View File

@ -36,7 +36,7 @@ namespace Goals
TGoalVec getAllPossibleSubgoals() override;
TSubgoal whatToDoToAchieve() override;
std::string completeMessage() const override;
virtual bool operator==(const GatherArmy & other) const override;
bool operator==(const GatherArmy & other) const override;
};
}

View File

@ -32,17 +32,17 @@ namespace Goals
return TGoalVec();
}
virtual bool operator==(const Invalid & other) const override
bool operator==(const Invalid & other) const override
{
return true;
}
virtual std::string toString() const override
std::string toString() const override
{
return "Invalid";
}
virtual void accept(AIGateway * ai) override
void accept(AIGateway * ai) override
{
throw cannotFulfillGoalException("Can not fulfill Invalid goal!");
}

View File

@ -38,12 +38,12 @@ namespace Goals
{
}
virtual bool operator==(const RecruitHero & other) const override
bool operator==(const RecruitHero & other) const override
{
return true;
}
virtual std::string toString() const override;
std::string toString() const override;
void accept(AIGateway * ai) override;
};
}

View File

@ -28,7 +28,7 @@ namespace Goals
void accept(AIGateway * ai) override;
std::string toString() const override;
virtual bool operator==(const SaveResources & other) const override;
bool operator==(const SaveResources & other) const override;
};
}

View File

@ -26,8 +26,8 @@ namespace Goals
public:
StayAtTown(const CGTownInstance * town, AIPath & path);
virtual bool operator==(const StayAtTown & other) const override;
virtual std::string toString() const override;
bool operator==(const StayAtTown & other) const override;
std::string toString() const override;
void accept(AIGateway * ai) override;
float getMovementWasted() const { return movementWasted; }
};

View File

@ -34,7 +34,7 @@ namespace Goals
value = val;
objid = Objid;
}
virtual bool operator==(const Trade & other) const override;
bool operator==(const Trade & other) const override;
};
}

View File

@ -29,8 +29,8 @@ namespace Goals
ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
virtual bool operator==(const ArmyUpgrade & other) const override;
virtual std::string toString() const override;
bool operator==(const ArmyUpgrade & other) const override;
std::string toString() const override;
uint64_t getUpgradeValue() const { return upgradeValue; }
uint64_t getInitialArmyValue() const { return initialValue; }

View File

@ -30,8 +30,8 @@ namespace Goals
DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false);
DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender);
virtual bool operator==(const DefendTown & other) const override;
virtual std::string toString() const override;
bool operator==(const DefendTown & other) const override;
std::string toString() const override;
const HitMapInfo & getTreat() const { return treat; }

View File

@ -28,8 +28,8 @@ namespace Goals
sethero(targetHero);
}
virtual bool operator==(const HeroExchange & other) const override;
virtual std::string toString() const override;
bool operator==(const HeroExchange & other) const override;
std::string toString() const override;
uint64_t getReinforcementArmyStrength() const;
};

View File

@ -36,8 +36,8 @@ namespace Goals
sethero(pathToCenter.targetHero);
}
virtual bool operator==(const UnlockCluster & other) const override;
virtual std::string toString() const override;
bool operator==(const UnlockCluster & other) const override;
std::string toString() const override;
std::shared_ptr<ObjectCluster> getCluster() const { return cluster; }
const AIPath & getPathToCenter() { return pathToCenter; }
};

View File

@ -332,7 +332,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
return neighbours;
}
EPathfindingLayer phisycalLayers[2] = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
constexpr std::array phisycalLayers = {EPathfindingLayer::LAND, EPathfindingLayer::SAIL};
bool AINodeStorage::increaseHeroChainTurnLimit()
{
@ -843,6 +843,7 @@ ExchangeCandidate HeroChainCalculationTask::calculateExchange(
candidate.turns = carrierParentNode->turns;
candidate.setCost(carrierParentNode->getCost() + otherParentNode->getCost() / 1000.0);
candidate.moveRemains = carrierParentNode->moveRemains;
candidate.danger = carrierParentNode->danger;
if(carrierParentNode->turns < otherParentNode->turns)
{
@ -1122,14 +1123,14 @@ void AINodeStorage::calculateTownPortal(
{
for(const CGTownInstance * targetTown : towns)
{
// TODO: allow to hide visiting hero in garrison
if(targetTown->visitingHero && maskMap.find(targetTown->visitingHero.get()) != maskMap.end())
if(targetTown->visitingHero
&& targetTown->getUpperArmy()->stacksCount()
&& maskMap.find(targetTown->visitingHero.get()) != maskMap.end())
{
auto basicMask = maskMap.at(targetTown->visitingHero.get());
bool heroIsInChain = (actor->chainMask & basicMask) != 0;
bool sameActorInTown = actor->chainMask == basicMask;
if(sameActorInTown || !heroIsInChain)
if(!sameActorInTown)
continue;
}
@ -1264,8 +1265,8 @@ bool AINodeStorage::hasBetterChain(
&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength
&& node.getCost() <= candidateNode->getCost())
{
if(nodeActor->heroFightingStrength == candidateActor->heroFightingStrength
&& node.getCost() == candidateNode->getCost()
if(vstd::isAlmostEqual(nodeActor->heroFightingStrength, candidateActor->heroFightingStrength)
&& vstd::isAlmostEqual(node.getCost(), candidateNode->getCost())
&& &node < candidateNode)
{
continue;

View File

@ -11,6 +11,7 @@
#pragma once
#define NKAI_PATHFINDER_TRACE_LEVEL 0
constexpr int NKAI_GRAPH_TRACE_LEVEL = 0;
#define NKAI_TRACE_LEVEL 0
#include "../../../lib/pathfinder/CGPathNode.h"
@ -24,20 +25,11 @@
namespace NKAI
{
const int SCOUT_TURN_DISTANCE_LIMIT = 5;
const int MAIN_TURN_DISTANCE_LIMIT = 10;
namespace AIPathfinding
{
#ifdef ENVIRONMENT64
const int BUCKET_COUNT = 7;
#else
const int BUCKET_COUNT = 5;
#endif // ENVIRONMENT64
const int BUCKET_COUNT = 3;
const int BUCKET_SIZE = 5;
const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
const int THREAD_COUNT = 8;
const int CHAIN_MAX_DEPTH = 4;
}
@ -188,7 +180,7 @@ public:
bool selectFirstActor();
bool selectNextActor();
virtual std::vector<CGPathNode *> getInitialNodes() override;
std::vector<CGPathNode *> getInitialNodes() override;
virtual std::vector<CGPathNode *> calculateNeighbours(
const PathNodeInfo & source,
@ -200,7 +192,7 @@ public:
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper) override;
virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override;
void commit(
AIPathNode * destination,

View File

@ -33,7 +33,7 @@ bool AIPathfinder::isTileAccessible(const HeroPtr & hero, const int3 & tile) con
|| storage->isTileAccessible(hero, tile, EPathfindingLayer::SAIL);
}
std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile) const
std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile, bool includeGraph) const
{
const TerrainTile * tileInfo = cb->getTile(tile, false);
@ -42,10 +42,23 @@ std::vector<AIPath> AIPathfinder::getPathInfo(const int3 & tile) const
return std::vector<AIPath>();
}
return storage->getChainInfo(tile, !tileInfo->isWater());
auto info = storage->getChainInfo(tile, !tileInfo->isWater());
if(includeGraph)
{
for(auto hero : cb->getHeroesInfo())
{
auto graph = heroGraphs.find(hero->id);
if(graph != heroGraphs.end())
graph->second.addChainInfo(info, tile, hero, ai);
}
}
return info;
}
void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, PathfinderSettings pathfinderSettings)
void AIPathfinder::updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings)
{
if(!storage)
{
@ -71,7 +84,7 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs);
}
auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage);
auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, storage, pathfinderSettings.allowBypassObjects);
logAi->trace("Recalculate paths pass %d", pass++);
cb->calculatePaths(config);
@ -112,4 +125,34 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
logAi->trace("Recalculated paths in %ld", timeElapsed(start));
}
void AIPathfinder::updateGraphs(const std::map<const CGHeroInstance *, HeroRole> & heroes)
{
auto start = std::chrono::high_resolution_clock::now();
std::vector<const CGHeroInstance *> heroesVector;
heroGraphs.clear();
for(auto hero : heroes)
{
if(heroGraphs.try_emplace(hero.first->id, GraphPaths()).second)
heroesVector.push_back(hero.first);
}
parallel_for(blocked_range<size_t>(0, heroesVector.size()), [this, &heroesVector](const blocked_range<size_t> & r)
{
for(auto i = r.begin(); i != r.end(); i++)
heroGraphs.at(heroesVector[i]->id).calculatePaths(heroesVector[i], ai);
});
if(NKAI_GRAPH_TRACE_LEVEL >= 1)
{
for(auto hero : heroes)
{
heroGraphs[hero.first->id].dumpToLog();
}
}
logAi->trace("Graph paths updated in %lld", timeElapsed(start));
}
}

View File

@ -11,6 +11,7 @@
#pragma once
#include "AINodeStorage.h"
#include "ObjectGraph.h"
#include "../AIUtility.h"
namespace NKAI
@ -23,11 +24,13 @@ struct PathfinderSettings
bool useHeroChain;
uint8_t scoutTurnDistanceLimit;
uint8_t mainTurnDistanceLimit;
bool allowBypassObjects;
PathfinderSettings()
:useHeroChain(false),
scoutTurnDistanceLimit(255),
mainTurnDistanceLimit(255)
mainTurnDistanceLimit(255),
allowBypassObjects(true)
{ }
};
@ -37,13 +40,20 @@ private:
std::shared_ptr<AINodeStorage> storage;
CPlayerSpecificInfoCallback * cb;
Nullkiller * ai;
std::map<ObjectInstanceID, GraphPaths> heroGraphs;
public:
AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai);
std::vector<AIPath> getPathInfo(const int3 & tile) const;
std::vector<AIPath> getPathInfo(const int3 & tile, bool includeGraph = false) const;
bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const;
void updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes, PathfinderSettings pathfinderSettings);
void updatePaths(const std::map<const CGHeroInstance *, HeroRole> & heroes, PathfinderSettings pathfinderSettings);
void updateGraphs(const std::map<const CGHeroInstance *, HeroRole> & heroes);
void init();
std::shared_ptr<AINodeStorage>getStorage()
{
return storage;
}
};
}

View File

@ -24,16 +24,17 @@ namespace AIPathfinding
std::vector<std::shared_ptr<IPathfindingRule>> makeRuleset(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage)
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects)
{
std::vector<std::shared_ptr<IPathfindingRule>> rules = {
std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
std::make_shared<DestinationActionRule>(),
std::make_shared<AIMovementToDestinationRule>(nodeStorage),
std::make_shared<MovementCostRule>(),
std::make_shared<AIPreviousNodeRule>(nodeStorage),
std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage)
};
std::vector<std::shared_ptr<IPathfindingRule>> rules = {
std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
std::make_shared<DestinationActionRule>(),
std::make_shared<AIMovementToDestinationRule>(nodeStorage, allowBypassObjects),
std::make_shared<MovementCostRule>(),
std::make_shared<AIPreviousNodeRule>(nodeStorage),
std::make_shared<AIMovementAfterDestinationRule>(cb, nodeStorage, allowBypassObjects)
};
return rules;
}
@ -41,8 +42,9 @@ namespace AIPathfinding
AIPathfinderConfig::AIPathfinderConfig(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage)
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage)
{
options.canUseCast = true;
}

View File

@ -30,11 +30,12 @@ namespace AIPathfinding
AIPathfinderConfig(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage);
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects);
~AIPathfinderConfig();
virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
};
}

View File

@ -29,7 +29,7 @@ namespace AIPathfinding
public:
AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero, DayFlags flagsToAdd = DayFlags::NONE);
virtual void execute(const CGHeroInstance * hero) const override;
void execute(const CGHeroInstance * hero) const override;
virtual void applyOnDestination(
const CGHeroInstance * hero,
@ -38,9 +38,9 @@ namespace AIPathfinding
AIPathNode * dstMode,
const AIPathNode * srcNode) const override;
virtual bool canAct(const AIPathNode * source) const override;
bool canAct(const AIPathNode * source) const override;
virtual std::string toString() const override;
std::string toString() const override;
};
class WaterWalkingAction : public AdventureCastAction

View File

@ -28,9 +28,9 @@ namespace AIPathfinding
{
}
virtual void execute(const CGHeroInstance * hero) const override;
void execute(const CGHeroInstance * hero) const override;
virtual std::string toString() const override;
std::string toString() const override;
};
}

View File

@ -25,7 +25,7 @@ namespace AIPathfinding
class SummonBoatAction : public VirtualBoatAction
{
public:
virtual void execute(const CGHeroInstance * hero) const override;
void execute(const CGHeroInstance * hero) const override;
virtual void applyOnDestination(
const CGHeroInstance * hero,
@ -34,11 +34,11 @@ namespace AIPathfinding
AIPathNode * dstMode,
const AIPathNode * srcNode) const override;
virtual bool canAct(const AIPathNode * source) const override;
bool canAct(const AIPathNode * source) const override;
virtual const ChainActor * getActor(const ChainActor * sourceActor) const override;
const ChainActor * getActor(const ChainActor * sourceActor) const override;
virtual std::string toString() const override;
std::string toString() const override;
private:
int32_t getManaCost(const CGHeroInstance * hero) const;
@ -56,17 +56,17 @@ namespace AIPathfinding
{
}
virtual bool canAct(const AIPathNode * source) const override;
bool canAct(const AIPathNode * source) const override;
virtual void execute(const CGHeroInstance * hero) const override;
void execute(const CGHeroInstance * hero) const override;
virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
virtual const ChainActor * getActor(const ChainActor * sourceActor) const override;
const ChainActor * getActor(const ChainActor * sourceActor) const override;
virtual std::string toString() const override;
std::string toString() const override;
virtual const CGObjectInstance * targetObject() const override;
const CGObjectInstance * targetObject() const override;
};
}

View File

@ -19,14 +19,19 @@ namespace NKAI
namespace AIPathfinding
{
bool QuestAction::canAct(const AIPathNode * node) const
{
return canAct(node->actor->hero);
}
bool QuestAction::canAct(const CGHeroInstance * hero) const
{
if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)
{
return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(node->actor->hero);
return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(hero);
}
return questInfo.quest->activeForPlayers.count(node->actor->hero->getOwner())
|| questInfo.quest->checkQuest(node->actor->hero);
return questInfo.quest->activeForPlayers.count(hero->getOwner())
|| questInfo.quest->checkQuest(hero);
}
Goals::TSubgoal QuestAction::decompose(const CGHeroInstance * hero) const

View File

@ -28,13 +28,15 @@ namespace AIPathfinding
{
}
virtual bool canAct(const AIPathNode * node) const override;
bool canAct(const AIPathNode * node) const override;
virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
bool canAct(const CGHeroInstance * hero) const;
virtual void execute(const CGHeroInstance * hero) const override;
Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
virtual std::string toString() const override;
void execute(const CGHeroInstance * hero) const override;
std::string toString() const override;
};
}

View File

@ -29,9 +29,9 @@ namespace AIPathfinding
{
}
virtual void execute(const CGHeroInstance * hero) const override;
void execute(const CGHeroInstance * hero) const override;
virtual std::string toString() const override;
std::string toString() const override;
};
}

View File

@ -18,7 +18,7 @@
using namespace NKAI;
CCreatureSet emptyArmy;
const CCreatureSet emptyArmy;
bool HeroExchangeArmy::needsLastStack() const
{

View File

@ -28,7 +28,7 @@ class HeroExchangeArmy : public CArmedInstance
public:
TResources armyCost;
bool requireBuyArmy;
virtual bool needsLastStack() const override;
bool needsLastStack() const override;
std::shared_ptr<SpecialAction> getActorAction() const;
HeroExchangeArmy(): CArmedInstance(nullptr, true), requireBuyArmy(false) {}
@ -51,24 +51,24 @@ protected:
public:
uint64_t chainMask;
bool isMovable;
bool allowUseResources;
bool allowBattle;
bool allowSpellCast;
bool isMovable = false;
bool allowUseResources = false;
bool allowBattle = false;
bool allowSpellCast = false;
std::shared_ptr<SpecialAction> actorAction;
const CGHeroInstance * hero;
HeroRole heroRole;
const CCreatureSet * creatureSet;
const ChainActor * battleActor;
const ChainActor * castActor;
const ChainActor * resourceActor;
const ChainActor * carrierParent;
const ChainActor * otherParent;
const ChainActor * baseActor;
const CCreatureSet * creatureSet = nullptr;
const ChainActor * battleActor = nullptr;
const ChainActor * castActor = nullptr;
const ChainActor * resourceActor = nullptr;
const ChainActor * carrierParent = nullptr;
const ChainActor * otherParent = nullptr;
const ChainActor * baseActor = nullptr;
int3 initialPosition;
EPathfindingLayer layer;
uint32_t initialMovement;
uint32_t initialTurn;
uint32_t initialMovement = 0;
uint32_t initialTurn = 0;
uint64_t armyValue;
float heroFightingStrength;
uint8_t actorExchangeCount;
@ -126,7 +126,7 @@ public:
HeroActor(const ChainActor * carrier, const ChainActor * other, const HeroExchangeArmy * army, const Nullkiller * ai);
protected:
virtual ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override;
ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const override;
};
class ObjectActor : public ChainActor
@ -136,7 +136,7 @@ private:
public:
ObjectActor(const CGObjectInstance * obj, const CCreatureSet * army, uint64_t chainMask, int initialTurn);
virtual std::string toString() const override;
std::string toString() const override;
const CGObjectInstance * getActorObject() const override;
};
@ -154,7 +154,7 @@ private:
public:
DwellingActor(const CGDwelling * dwelling, uint64_t chainMask, bool waitForGrowth, int dayOfWeek);
~DwellingActor();
virtual std::string toString() const override;
std::string toString() const override;
protected:
int getInitialTurn(bool waitForGrowth, int dayOfWeek);
@ -168,7 +168,7 @@ private:
public:
TownGarrisonActor(const CGTownInstance * town, uint64_t chainMask);
virtual std::string toString() const override;
std::string toString() const override;
};
}

View File

@ -0,0 +1,671 @@
/*
* ObjectGraph.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 "ObjectGraph.h"
#include "AIPathfinderConfig.h"
#include "../../../lib/CRandomGenerator.h"
#include "../../../CCallback.h"
#include "../../../lib/mapping/CMap.h"
#include "../Engine/Nullkiller.h"
#include "../../../lib/logging/VisualLogger.h"
#include "Actions/QuestAction.h"
namespace NKAI
{
struct ConnectionCostInfo
{
float totalCost = 0;
float avg = 0;
int connectionsCount = 0;
};
class ObjectGraphCalculator
{
private:
ObjectGraph * target;
const Nullkiller * ai;
std::map<const CGHeroInstance *, HeroRole> actors;
std::map<const CGHeroInstance *, const CGObjectInstance *> actorObjectMap;
std::vector<std::unique_ptr<CGBoat>> temporaryBoats;
std::vector<std::unique_ptr<CGHeroInstance>> temporaryActorHeroes;
public:
ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
:ai(ai), target(target)
{
}
void setGraphObjects()
{
for(auto obj : ai->memory->visitableObjs)
{
if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
{
addObjectActor(obj);
}
}
for(auto town : ai->cb->getTownsInfo())
{
addObjectActor(town);
}
}
void calculateConnections()
{
updatePaths();
foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
{
calculateConnections(pos);
});
removeExtraConnections();
}
void addMinimalDistanceJunctions()
{
foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
{
if(target->hasNodeAt(pos))
return;
if(ai->cb->getGuardingCreaturePosition(pos).valid())
return;
ConnectionCostInfo currentCost = getConnectionsCost(pos);
if(currentCost.connectionsCount <= 2)
return;
float neighborCost = currentCost.avg + 0.001f;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Checking junction %s", pos.toString());
}
foreach_neighbour(
ai->cb.get(),
pos,
[this, &neighborCost](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
auto costTotal = this->getConnectionsCost(neighbor);
if(costTotal.avg < neighborCost)
{
neighborCost = costTotal.avg;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Better node found at %s", neighbor.toString());
}
}
});
if(currentCost.avg < neighborCost)
{
addJunctionActor(pos);
}
});
}
private:
void updatePaths()
{
PathfinderSettings ps;
ps.mainTurnDistanceLimit = 5;
ps.scoutTurnDistanceLimit = 1;
ps.allowBypassObjects = false;
ai->pathfinder->updatePaths(actors, ps);
}
void calculateConnections(const int3 & pos)
{
if(target->hasNodeAt(pos))
{
foreach_neighbour(
ai->cb.get(),
pos,
[this, &pos](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
if(target->hasNodeAt(neighbor))
{
auto paths = ai->pathfinder->getPathInfo(neighbor);
for(auto & path : paths)
{
if(pos == path.targetHero->visitablePos())
{
target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger());
}
}
}
});
return;
}
auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
auto paths = ai->pathfinder->getPathInfo(pos);
for(AIPath & path1 : paths)
{
for(AIPath & path2 : paths)
{
if(path1.targetHero == path2.targetHero)
continue;
auto pos1 = path1.targetHero->visitablePos();
auto pos2 = path2.targetHero->visitablePos();
if(guardPos.valid() && guardPos != pos1 && guardPos != pos2)
continue;
auto obj1 = actorObjectMap[path1.targetHero];
auto obj2 = actorObjectMap[path2.targetHero];
auto tile1 = cb->getTile(pos1);
auto tile2 = cb->getTile(pos2);
if(tile2->isWater() && !tile1->isWater())
{
if(!cb->getTile(pos)->isWater())
continue;
if(obj1 && (obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD))
continue;
}
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
auto updated = target->tryAddConnection(
pos1,
pos2,
path1.movementCost() + path2.movementCost(),
danger);
if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated)
{
logAi->trace(
"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
obj1 ? obj1->getObjectName() : "J", pos1.toString(),
obj2 ? obj2->getObjectName() : "J", pos2.toString(),
pos.toString(),
path1.movementCost() + path2.movementCost());
}
}
}
}
bool isExtraConnection(float direct, float side1, float side2) const
{
float sideRatio = (side1 + side2) / direct;
return sideRatio < 1.25f && direct > side1 && direct > side2;
}
void removeExtraConnections()
{
std::vector<std::pair<int3, int3>> connectionsToRemove;
for(auto & actor : temporaryActorHeroes)
{
auto pos = actor->visitablePos();
auto & currentNode = target->getNode(pos);
target->iterateConnections(pos, [this, &pos, &connectionsToRemove, &currentNode](int3 n1, ObjectLink o1)
{
target->iterateConnections(n1, [&pos, &o1, &currentNode, &connectionsToRemove, this](int3 n2, ObjectLink o2)
{
auto direct = currentNode.connections.find(n2);
if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost))
{
connectionsToRemove.push_back({pos, n2});
}
});
});
}
vstd::removeDuplicates(connectionsToRemove);
for(auto & c : connectionsToRemove)
{
target->removeConnection(c.first, c.second);
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString());
}
}
}
void addObjectActor(const CGObjectInstance * obj)
{
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(obj->cb)).get();
CRandomGenerator rng;
auto visitablePos = obj->visitablePos();
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
if(cb->getTile(visitablePos)->isWater())
{
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = obj;
actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD ? HeroRole::MAIN : HeroRole::SCOUT;
target->addObject(obj);
}
void addJunctionActor(const int3 & visitablePos)
{
auto internalCb = temporaryActorHeroes.front()->cb;
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
CRandomGenerator rng;
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
if(cb->getTile(visitablePos)->isWater())
{
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = nullptr;
actors[objectActor] = HeroRole::SCOUT;
target->registerJunction(visitablePos);
}
ConnectionCostInfo getConnectionsCost(const int3 & pos) const
{
auto paths = ai->pathfinder->getPathInfo(pos);
std::map<int3, float> costs;
for(auto & path : paths)
{
auto fromPos = path.targetHero->visitablePos();
auto cost = costs.find(fromPos);
if(cost == costs.end())
{
costs.emplace(fromPos, path.movementCost());
}
else
{
if(path.movementCost() < cost->second)
{
costs[fromPos] = path.movementCost();
}
}
}
ConnectionCostInfo result;
for(auto & cost : costs)
{
result.totalCost += cost.second;
result.connectionsCount++;
}
if(result.connectionsCount)
{
result.avg = result.totalCost / result.connectionsCount;
}
return result;
}
};
bool ObjectGraph::tryAddConnection(
const int3 & from,
const int3 & to,
float cost,
uint64_t danger)
{
return nodes[from].connections[to].update(cost, danger);
}
void ObjectGraph::removeConnection(const int3 & from, const int3 & to)
{
nodes[from].connections.erase(to);
}
void ObjectGraph::updateGraph(const Nullkiller * ai)
{
auto cb = ai->cb;
ObjectGraphCalculator calculator(this, ai);
calculator.setGraphObjects();
calculator.calculateConnections();
calculator.addMinimalDistanceJunctions();
calculator.calculateConnections();
if(NKAI_GRAPH_TRACE_LEVEL >= 1)
dumpToLog("graph");
}
void ObjectGraph::addObject(const CGObjectInstance * obj)
{
nodes[obj->visitablePos()].init(obj);
}
void ObjectGraph::registerJunction(const int3 & pos)
{
nodes[pos].initJunction();
}
void ObjectGraph::removeObject(const CGObjectInstance * obj)
{
nodes[obj->visitablePos()].objectExists = false;
if(obj->ID == Obj::BOAT)
{
vstd::erase_if(nodes[obj->visitablePos()].connections, [&](const std::pair<int3, ObjectLink> & link) -> bool
{
auto tile = cb->getTile(link.first, false);
return tile && tile->isWater();
});
}
}
void ObjectGraph::connectHeroes(const Nullkiller * ai)
{
for(auto obj : ai->memory->visitableObjs)
{
if(obj && obj->ID == Obj::HERO)
{
addObject(obj);
}
}
for(auto & node : nodes)
{
auto pos = node.first;
auto paths = ai->pathfinder->getPathInfo(pos);
for(AIPath & path : paths)
{
if(path.getFirstBlockedAction())
continue;
auto heroPos = path.targetHero->visitablePos();
nodes[pos].connections[heroPos].update(
path.movementCost(),
path.getPathDanger());
nodes[heroPos].connections[pos].update(
path.movementCost(),
path.getPathDanger());
}
}
}
void ObjectGraph::dumpToLog(std::string visualKey) const
{
logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder)
{
for(auto & tile : nodes)
{
for(auto & node : tile.second.connections)
{
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace(
"%s -> %s: %f !%d",
node.first.toString(),
tile.first.toString(),
node.second.cost,
node.second.danger);
}
logBuilder.addLine(tile.first, node.first);
}
}
});
}
bool GraphNodeComparer::operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const
{
return pathNodes.at(lhs.coord)[lhs.nodeType].cost > pathNodes.at(rhs.coord)[rhs.nodeType].cost;
}
void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
{
graph = *ai->baseGraph;
graph.connectHeroes(ai);
visualKey = std::to_string(ai->playerID) + ":" + targetHero->getNameTranslated();
pathNodes.clear();
GraphNodeComparer cmp(pathNodes);
GraphPathNode::TFibHeap pq(cmp);
pathNodes[targetHero->visitablePos()][GrapthPathNodeType::NORMAL].cost = 0;
pq.emplace(GraphPathNodePointer(targetHero->visitablePos(), GrapthPathNodeType::NORMAL));
while(!pq.empty())
{
GraphPathNodePointer pos = pq.top();
pq.pop();
auto & node = getOrCreateNode(pos);
std::shared_ptr<SpecialAction> transitionAction;
if(node.obj)
{
if(node.obj->ID == Obj::QUEST_GUARD
|| node.obj->ID == Obj::BORDERGUARD
|| node.obj->ID == Obj::BORDER_GATE)
{
auto questObj = dynamic_cast<const IQuestObject *>(node.obj);
auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord);
if(node.obj->ID == Obj::QUEST_GUARD
&& questObj->quest->mission == Rewardable::Limiter{}
&& questObj->quest->killTarget == ObjectInstanceID::NONE)
{
continue;
}
auto questAction = std::make_shared<AIPathfinding::QuestAction>(questInfo);
if(!questAction->canAct(targetHero))
{
transitionAction = questAction;
}
}
}
node.isInQueue = false;
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, ObjectLink o)
{
auto targetNodeType = o.danger || transitionAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
auto targetPointer = GraphPathNodePointer(target, targetNodeType);
auto & targetNode = getOrCreateNode(targetPointer);
if(targetNode.tryUpdate(pos, node, o))
{
targetNode.specialAction = transitionAction;
auto targetGraphNode = graph.getNode(target);
if(targetGraphNode.objID.hasValue())
{
targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false);
if(targetNode.obj && targetNode.obj->ID == Obj::HERO)
return;
}
if(targetNode.isInQueue)
{
pq.increase(targetNode.handle);
}
else
{
targetNode.handle = pq.emplace(targetPointer);
targetNode.isInQueue = true;
}
}
});
}
}
void GraphPaths::dumpToLog() const
{
logVisual->updateWithLock(visualKey, [&](IVisualLogBuilder & logBuilder)
{
for(auto & tile : pathNodes)
{
for(auto & node : tile.second)
{
if(!node.previous.valid())
continue;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace(
"%s -> %s: %f !%d",
node.previous.coord.toString(),
tile.first.toString(),
node.cost,
node.danger);
}
logBuilder.addLine(node.previous.coord, tile.first);
}
}
});
}
bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link)
{
auto newCost = prev.cost + link.cost;
if(newCost < cost)
{
previous = pos;
danger = prev.danger + link.danger;
cost = newCost;
return true;
}
return false;
}
void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const
{
auto nodes = pathNodes.find(tile);
if(nodes == pathNodes.end())
return;
for(auto & node : nodes->second)
{
if(!node.reachable())
continue;
std::vector<GraphPathNodePointer> tilesToPass;
uint64_t danger = node.danger;
float cost = node.cost;
bool allowBattle = false;
auto current = GraphPathNodePointer(nodes->first, node.nodeType);
while(true)
{
auto currentTile = pathNodes.find(current.coord);
if(currentTile == pathNodes.end())
break;
auto currentNode = currentTile->second[current.nodeType];
if(!currentNode.previous.valid())
break;
allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
vstd::amax(danger, currentNode.danger);
vstd::amax(cost, currentNode.cost);
tilesToPass.push_back(current);
if(currentNode.cost < 2.0f)
break;
current = currentNode.previous;
}
if(tilesToPass.empty())
continue;
auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
for(auto & path : entryPaths)
{
if(path.targetHero != hero)
continue;
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
{
AIPathNodeInfo n;
n.coord = graphTile->coord;
n.cost = cost;
n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
n.danger = danger;
n.targetHero = hero;
n.parentIndex = -1;
n.specialAction = getNode(*graphTile).specialAction;
for(auto & node : path.nodes)
{
node.parentIndex++;
}
path.nodes.insert(path.nodes.begin(), n);
}
path.armyLoss += ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
paths.push_back(path);
}
}
}
}

View File

@ -0,0 +1,192 @@
/*
* ObjectGraph.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
#include "AINodeStorage.h"
#include "../AIUtility.h"
namespace NKAI
{
class Nullkiller;
struct ObjectLink
{
float cost = 100000; // some big number
uint64_t danger = 0;
bool update(float newCost, uint64_t newDanger)
{
if(cost > newCost)
{
cost = newCost;
danger = newDanger;
return true;
}
return false;
}
};
struct ObjectNode
{
ObjectInstanceID objID;
MapObjectID objTypeID;
bool objectExists;
std::unordered_map<int3, ObjectLink> connections;
void init(const CGObjectInstance * obj)
{
objectExists = true;
objID = obj->id;
objTypeID = obj->ID;
}
void initJunction()
{
objectExists = false;
objID = ObjectInstanceID();
objTypeID = Obj();
}
};
class ObjectGraph
{
std::unordered_map<int3, ObjectNode> nodes;
public:
void updateGraph(const Nullkiller * ai);
void addObject(const CGObjectInstance * obj);
void registerJunction(const int3 & pos);
void connectHeroes(const Nullkiller * ai);
void removeObject(const CGObjectInstance * obj);
bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
void removeConnection(const int3 & from, const int3 & to);
void dumpToLog(std::string visualKey) const;
template<typename Func>
void iterateConnections(const int3 & pos, Func fn)
{
for(auto & connection : nodes.at(pos).connections)
{
fn(connection.first, connection.second);
}
}
const ObjectNode & getNode(int3 tile) const
{
return nodes.at(tile);
}
bool hasNodeAt(const int3 & tile) const
{
return vstd::contains(nodes, tile);
}
};
struct GraphPathNode;
enum GrapthPathNodeType
{
NORMAL,
BATTLE,
LAST
};
struct GraphPathNodePointer
{
int3 coord = int3(-1);
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
GraphPathNodePointer() = default;
GraphPathNodePointer(int3 coord, GrapthPathNodeType type)
:coord(coord), nodeType(type)
{ }
bool valid() const
{
return coord.valid();
}
};
typedef std::unordered_map<int3, GraphPathNode[GrapthPathNodeType::LAST]> GraphNodeStorage;
class GraphNodeComparer
{
const GraphNodeStorage & pathNodes;
public:
GraphNodeComparer(const GraphNodeStorage & pathNodes)
:pathNodes(pathNodes)
{
}
bool operator()(const GraphPathNodePointer & lhs, const GraphPathNodePointer & rhs) const;
};
struct GraphPathNode
{
const float BAD_COST = 100000;
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
GraphPathNodePointer previous;
float cost = BAD_COST;
uint64_t danger = 0;
const CGObjectInstance * obj = nullptr;
std::shared_ptr<SpecialAction> specialAction;
using TFibHeap = boost::heap::fibonacci_heap<GraphPathNodePointer, boost::heap::compare<GraphNodeComparer>>;
TFibHeap::handle_type handle;
bool isInQueue = false;
bool reachable() const
{
return cost < BAD_COST;
}
bool tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link);
};
class GraphPaths
{
ObjectGraph graph;
GraphNodeStorage pathNodes;
std::string visualKey;
public:
void calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai);
void addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHeroInstance * hero, const Nullkiller * ai) const;
void dumpToLog() const;
private:
GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos)
{
auto & node = pathNodes[pos.coord][pos.nodeType];
node.nodeType = pos.nodeType;
return node;
}
const GraphPathNode & getNode(const GraphPathNodePointer & pos) const
{
auto & node = pathNodes.at(pos.coord)[pos.nodeType];
return node;
}
};
}

View File

@ -131,7 +131,7 @@ namespace AIPathfinding
{
if(obj->ID != Obj::TOWN) //towns were handled in the previous loop
{
if(const IShipyard * shipyard = IShipyard::castFrom(obj))
if(const auto * shipyard = dynamic_cast<const IShipyard *>(obj))
shipyards.push_back(shipyard);
}
}

View File

@ -13,6 +13,7 @@
#include "../Actions/QuestAction.h"
#include "../../Goals/Invalid.h"
#include "AIPreviousNodeRule.h"
#include "../../../../lib/pathfinder/PathfinderOptions.h"
namespace NKAI
{
@ -20,8 +21,9 @@ namespace AIPathfinding
{
AIMovementAfterDestinationRule::AIMovementAfterDestinationRule(
CPlayerSpecificInfoCallback * cb,
std::shared_ptr<AINodeStorage> nodeStorage)
:cb(cb), nodeStorage(nodeStorage)
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects)
:cb(cb), nodeStorage(nodeStorage), allowBypassObjects(allowBypassObjects)
{
}
@ -40,10 +42,32 @@ namespace AIPathfinding
}
auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
if(blocker == BlockingReason::NONE)
{
destination.blocked = nodeStorage->isDistanceLimitReached(source, destination);
if(destination.nodeObject
&& !destination.blocked
&& !allowBypassObjects
&& !dynamic_cast<const CGTeleport *>(destination.nodeObject)
&& destination.nodeObject->ID != Obj::EVENT)
{
destination.blocked = true;
destination.node->locked = true;
}
return;
}
if(!allowBypassObjects)
{
if(destination.nodeObject)
{
destination.blocked = true;
destination.node->locked = true;
}
return;
}

View File

@ -25,9 +25,13 @@ namespace AIPathfinding
private:
CPlayerSpecificInfoCallback * cb;
std::shared_ptr<AINodeStorage> nodeStorage;
bool allowBypassObjects;
public:
AIMovementAfterDestinationRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr<AINodeStorage> nodeStorage);
AIMovementAfterDestinationRule(
CPlayerSpecificInfoCallback * cb,
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects);
virtual void process(
const PathNodeInfo & source,

View File

@ -14,8 +14,10 @@ namespace NKAI
{
namespace AIPathfinding
{
AIMovementToDestinationRule::AIMovementToDestinationRule(std::shared_ptr<AINodeStorage> nodeStorage)
: nodeStorage(nodeStorage)
AIMovementToDestinationRule::AIMovementToDestinationRule(
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects)
: nodeStorage(nodeStorage), allowBypassObjects(allowBypassObjects)
{
}
@ -37,15 +39,30 @@ namespace AIPathfinding
return;
}
if(blocker == BlockingReason::SOURCE_GUARDED && nodeStorage->getAINode(source.node)->actor->allowBattle)
if(blocker == BlockingReason::SOURCE_GUARDED)
{
auto actor = nodeStorage->getAINode(source.node)->actor;
if(!allowBypassObjects)
{
if (source.node->getCost() < 0.0001f)
return;
// when actor represents moster graph node, we need to let him escape monster
if(cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition)
return;
}
if(actor->allowBattle)
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 1
logAi->trace(
"Bypass src guard while moving from %s to %s",
source.coord.toString(),
destination.coord.toString());
logAi->trace(
"Bypass src guard while moving from %s to %s",
source.coord.toString(),
destination.coord.toString());
#endif
return;
return;
}
}
destination.blocked = true;

View File

@ -24,9 +24,10 @@ namespace AIPathfinding
{
private:
std::shared_ptr<AINodeStorage> nodeStorage;
bool allowBypassObjects;
public:
AIMovementToDestinationRule(std::shared_ptr<AINodeStorage> nodeStorage);
AIMovementToDestinationRule(std::shared_ptr<AINodeStorage> nodeStorage, bool allowBypassObjects);
virtual void process(
const PathNodeInfo & source,

View File

@ -14,7 +14,7 @@
#define strcpy_s(a, b, c) strncpy(a, c, b)
#endif
static const char * g_cszAiName = "Nullkiller";
static const char * const g_cszAiName = "Nullkiller";
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{

View File

@ -8,19 +8,19 @@ set(stupidAI_HEADERS
StupidAI.h
)
if(NOT ENABLE_STATIC_AI_LIBS)
if(NOT ENABLE_STATIC_LIBS)
list(APPEND stupidAI_SRCS main.cpp StdInc.cpp)
endif()
assign_source_group(${stupidAI_SRCS} ${stupidAI_HEADERS})
if(ENABLE_STATIC_AI_LIBS)
if(ENABLE_STATIC_LIBS)
add_library(StupidAI STATIC ${stupidAI_SRCS} ${stupidAI_HEADERS})
else()
add_library(StupidAI SHARED ${stupidAI_SRCS} ${stupidAI_HEADERS})
install(TARGETS StupidAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
endif()
target_link_libraries(StupidAI PRIVATE ${VCMI_LIB_TARGET})
target_link_libraries(StupidAI PRIVATE vcmi)
target_include_directories(StupidAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
vcmi_set_output_dir(StupidAI "AI")

View File

@ -16,7 +16,7 @@
#define strcpy_s(a, b, c) strncpy(a, c, b)
#endif
static const char *g_cszAiName = "Stupid AI 0.1";
static const char * const g_cszAiName = "Stupid AI 0.1";
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{

View File

@ -148,7 +148,7 @@ BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_
static const std::vector<BuildingID> unitGrowth = { BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR };
static const std::vector<BuildingID> _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3,
BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 };
static const std::vector<BuildingID> extra = { BuildingID::MARKETPLACE, BuildingID::BLACKSMITH, BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2,
static const std::vector<BuildingID> extra = { BuildingID::MARKETPLACE, BuildingID::BLACKSMITH, BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2,
BuildingID::SPECIAL_3, BuildingID::SPECIAL_4, BuildingID::SHIPYARD }; // all remaining buildings
bool BuildingManager::getBuildingOptions(const CGTownInstance * t)

View File

@ -94,12 +94,12 @@ set(VCAI_HEADERS
VCAI.h
)
if(NOT ENABLE_STATIC_AI_LIBS)
if(NOT ENABLE_STATIC_LIBS)
list(APPEND VCAI_SRCS main.cpp StdInc.cpp)
endif()
assign_source_group(${VCAI_SRCS} ${VCAI_HEADERS})
if(ENABLE_STATIC_AI_LIBS)
if(ENABLE_STATIC_LIBS)
add_library(VCAI STATIC ${VCAI_SRCS} ${VCAI_HEADERS})
else()
add_library(VCAI SHARED ${VCAI_SRCS} ${VCAI_HEADERS})
@ -107,7 +107,7 @@ else()
endif()
target_include_directories(VCAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(VCAI PUBLIC ${VCMI_LIB_TARGET} fuzzylite::fuzzylite)
target_link_libraries(VCAI PUBLIC vcmi fuzzylite::fuzzylite)
vcmi_set_output_dir(VCAI "AI")
enable_pch(VCAI)

View File

@ -109,51 +109,52 @@ bool AbstractGoal::operator==(const AbstractGoal & g) const
return false;
}
bool AbstractGoal::operator<(AbstractGoal & g) //for std::unique
{
//TODO: make sure it gets goals consistent with == operator
if (goalType < g.goalType)
return true;
if (goalType > g.goalType)
return false;
if (hero < g.hero)
return true;
if (hero > g.hero)
return false;
if (tile < g.tile)
return true;
if (g.tile < tile)
return false;
if (objid < g.objid)
return true;
if (objid > g.objid)
return false;
if (town < g.town)
return true;
if (town > g.town)
return false;
if (value < g.value)
return true;
if (value > g.value)
return false;
if (priority < g.priority)
return true;
if (priority > g.priority)
return false;
if (resID < g.resID)
return true;
if (resID > g.resID)
return false;
if (bid < g.bid)
return true;
if (bid > g.bid)
return false;
if (aid < g.aid)
return true;
if (aid > g.aid)
return false;
return false;
}
// FIXME: unused code?
//bool AbstractGoal::operator<(AbstractGoal & g) //for std::unique
//{
// //TODO: make sure it gets goals consistent with == operator
// if (goalType < g.goalType)
// return true;
// if (goalType > g.goalType)
// return false;
// if (hero < g.hero)
// return true;
// if (hero > g.hero)
// return false;
// if (tile < g.tile)
// return true;
// if (g.tile < tile)
// return false;
// if (objid < g.objid)
// return true;
// if (objid > g.objid)
// return false;
// if (town < g.town)
// return true;
// if (town > g.town)
// return false;
// if (value < g.value)
// return true;
// if (value > g.value)
// return false;
// if (priority < g.priority)
// return true;
// if (priority > g.priority)
// return false;
// if (resID < g.resID)
// return true;
// if (resID > g.resID)
// return false;
// if (bid < g.bid)
// return true;
// if (bid > g.bid)
// return false;
// if (aid < g.aid)
// return true;
// if (aid > g.aid)
// return false;
// return false;
//}
//TODO: find out why the following are not generated automatically on MVS?
bool TSubgoal::operator==(const TSubgoal & rhs) const

View File

@ -165,16 +165,16 @@ namespace Goals
virtual float accept(FuzzyHelper * f);
virtual bool operator==(const AbstractGoal & g) const;
bool operator<(AbstractGoal & g); //final
// bool operator<(AbstractGoal & g); //final
virtual bool fulfillsMe(Goals::TSubgoal goal) //TODO: multimethod instead of type check
{
return false; //use this method to check if goal is fulfilled by another (not equal) goal, operator == is handled spearately
}
bool operator!=(const AbstractGoal & g) const
{
return !(*this == g);
}
// bool operator!=(const AbstractGoal & g) const
// {
// return !(*this == g);
// }
template<typename Handler> void serialize(Handler & h)
{

View File

@ -39,6 +39,6 @@ namespace Goals
void accept(VCAI * ai) override;
std::string name() const override;
std::string completeMessage() const override;
virtual bool operator==(const AdventureSpellCast & other) const override;
bool operator==(const AdventureSpellCast & other) const override;
};
}

View File

@ -29,7 +29,7 @@ namespace Goals
TSubgoal whatToDoToAchieve() override;
bool fulfillsMe(TSubgoal goal) override;
virtual bool operator==(const Build & other) const override
bool operator==(const Build & other) const override
{
return true;
}

Some files were not shown because too many files have changed in this diff Show More