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

Merge pull request #4220 from IvanSavenko/ai_optimize

[1.5.4] AI optimizations
This commit is contained in:
Ivan Savenko 2024-07-05 15:45:49 +03:00 committed by GitHub
commit 11a3da3f4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 140 additions and 119 deletions

View File

@ -649,7 +649,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
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);
logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name(), ratio);
if(cb->getObj(goalObjectID, false))
{
@ -1574,7 +1574,7 @@ void AIGateway::requestActionASAP(std::function<void()> whatToDo)
void AIGateway::lostHero(HeroPtr h)
{
logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name);
logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name());
}
void AIGateway::answerQuery(QueryID queryID, int selection)

View File

@ -67,7 +67,6 @@ HeroPtr::HeroPtr(const CGHeroInstance * H)
}
h = H;
name = h->getNameTranslated();
hid = H->id;
// infosCount[ai->playerID][hid]++;
}
@ -89,6 +88,14 @@ bool HeroPtr::operator<(const HeroPtr & rhs) const
return hid < rhs.hid;
}
std::string HeroPtr::name() const
{
if (h)
return h->getNameTextID();
else
return "<NO HERO>";
}
const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const
{
return get(cb, doWeExpectNull);

View File

@ -87,8 +87,7 @@ struct DLL_EXPORT HeroPtr
ObjectInstanceID hid;
public:
std::string name;
std::string name() const;
HeroPtr();
HeroPtr(const CGHeroInstance * H);
@ -117,7 +116,6 @@ public:
{
handler & h;
handler & hid;
handler & name;
}
};

View File

@ -176,7 +176,7 @@ int HeroManager::selectBestSkill(const HeroPtr & hero, const std::vector<Seconda
logAi->trace(
"Hero %s is proposed to learn %d with score %f",
hero.name,
hero.name(),
skills[i].toEnum(),
score);
}
@ -204,6 +204,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
{
auto cached = knownFightingStrength.find(hero->id);
//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
}

View File

@ -212,7 +212,7 @@ void CaptureObjectsBehavior::decomposeObjects(
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects));
}
std::lock_guard<std::mutex> lock(sync);
std::lock_guard<std::mutex> lock(sync); // FIXME: consider using tbb::parallel_reduce instead to avoid mutex overhead
vstd::concatenate(result, tasksLocal);
});
}

View File

@ -31,7 +31,7 @@ namespace Goals
{
objid = obj->id.getNum();
tile = obj->visitablePos();
name = obj->getObjectName();
name = obj->typeName;
}
bool operator==(const CaptureObject & other) const override;

View File

@ -26,7 +26,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
if(obj)
{
objid = obj->id.getNum();
targetName = obj->getObjectName() + tile.toString();
targetName = obj->typeName + tile.toString();
}
else
{
@ -106,7 +106,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
if(!heroPtr.validAndSet())
{
logAi->error("Hero %s was lost. Exit hero chain.", heroPtr.name);
logAi->error("Hero %s was lost. Exit hero chain.", heroPtr.name());
return;
}
@ -143,7 +143,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
if(!heroPtr.validAndSet())
{
logAi->error("Hero %s was lost trying to execute special action. Exit hero chain.", heroPtr.name);
logAi->error("Hero %s was lost trying to execute special action. Exit hero chain.", heroPtr.name());
return;
}
@ -204,7 +204,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
{
if(!heroPtr.validAndSet())
{
logAi->error("Hero %s was lost. Exit hero chain.", heroPtr.name);
logAi->error("Hero %s was lost. Exit hero chain.", heroPtr.name());
return;
}
@ -250,7 +250,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
{
if(!heroPtr.validAndSet())
{
logAi->debug("Hero %s was killed while attempting to reach %s", heroPtr.name, node->coord.toString());
logAi->debug("Hero %s was killed while attempting to reach %s", heroPtr.name(), node->coord.toString());
return;
}

View File

@ -70,11 +70,8 @@ bool ExplorationHelper::scanMap()
int3 mapSize = cbp->getMapSize();
int perimeter = 2 * sightRadius * (mapSize.x + mapSize.y);
std::vector<int3> from;
std::vector<int3> to;
from.reserve(perimeter);
to.reserve(perimeter);
std::vector<int3> edgeTiles;
edgeTiles.reserve(perimeter);
foreach_tile_pos([&](const int3 & pos)
{
@ -91,13 +88,13 @@ bool ExplorationHelper::scanMap()
});
if(hasInvisibleNeighbor)
from.push_back(pos);
edgeTiles.push_back(pos);
}
});
logAi->debug("Exploration scan visible area perimeter for hero %s", hero->getNameTranslated());
for(const int3 & tile : from)
for(const int3 & tile : edgeTiles)
{
scanTile(tile);
}
@ -108,19 +105,36 @@ bool ExplorationHelper::scanMap()
}
allowDeadEndCancellation = false;
for(int i = 0; i < sightRadius; i++)
{
getVisibleNeighbours(from, to);
vstd::concatenate(from, to);
vstd::removeDuplicates(from);
}
logAi->debug("Exploration scan all possible tiles for hero %s", hero->getNameTranslated());
for(const int3 & tile : from)
boost::multi_array<ui8, 3> potentialTiles = *ts->fogOfWarMap;
std::vector<int3> tilesToExploreFrom = edgeTiles;
// WARNING: POTENTIAL BUG
// AI attempts to move to any tile within sight radius to reveal some new tiles
// however sight radius is circular, while this method assumes square radius
// standing on the edge of a square will NOT reveal tile in opposite corner
for(int i = 0; i < sightRadius; i++)
{
scanTile(tile);
std::vector<int3> newTilesToExploreFrom;
for(const int3 & tile : tilesToExploreFrom)
{
foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour)
{
if(potentialTiles[neighbour.z][neighbour.x][neighbour.y])
{
newTilesToExploreFrom.push_back(neighbour);
potentialTiles[neighbour.z][neighbour.x][neighbour.y] = false;
}
});
}
for(const int3 & tile : newTilesToExploreFrom)
{
scanTile(tile);
}
std::swap(tilesToExploreFrom, newTilesToExploreFrom);
}
return !bestGoal->invalid();
@ -172,20 +186,6 @@ void ExplorationHelper::scanTile(const int3 & tile)
}
}
void ExplorationHelper::getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const
{
for(const int3 & tile : tiles)
{
foreach_neighbour(cbp, tile, [&](CCallback * cbp, int3 neighbour)
{
if((*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
{
out.push_back(neighbour);
}
});
}
}
int ExplorationHelper::howManyTilesWillBeDiscovered(const int3 & pos) const
{
int ret = 0;

View File

@ -46,7 +46,6 @@ public:
private:
void scanTile(const int3 & tile);
bool hasReachableNeighbor(const int3 & pos) const;
void getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const;
};
}

View File

@ -320,11 +320,9 @@ void AINodeStorage::calculateNeighbours(
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper)
{
std::vector<int3> accessibleNeighbourTiles;
NeighbourTilesVector accessibleNeighbourTiles;
result.clear();
accessibleNeighbourTiles.reserve(8);
pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);
const AIPathNode * srcNode = getAINode(source.node);

View File

@ -23,6 +23,8 @@ constexpr int NKAI_GRAPH_TRACE_LEVEL = 0;
#include "Actions/SpecialAction.h"
#include "Actors.h"
#include <boost/container/small_vector.hpp>
namespace NKAI
{
namespace AIPathfinding
@ -85,7 +87,9 @@ struct AIPathNodeInfo
struct AIPath
{
std::vector<AIPathNodeInfo> nodes;
using NodesVector = boost::container::small_vector<AIPathNodeInfo, 16>;
NodesVector nodes;
uint64_t targetObjectDanger;
uint64_t armyLoss;
uint64_t targetObjectArmyLoss;

View File

@ -141,7 +141,8 @@ namespace AIPathfinding
{
SpellID summonBoat = SpellID::SUMMON_BOAT;
return hero->getSpellCost(summonBoat.toSpell());
// FIXME: this should be hero->getSpellCost, however currently queries to bonus system are too slow
return summonBoat.toSpell()->getCost(0);
}
}

View File

@ -118,7 +118,7 @@ void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkil
targetNode.specialAction = compositeAction;
auto targetGraphNode = graph.getNode(target);
const auto & targetGraphNode = graph.getNode(target);
if(targetGraphNode.objID.hasValue())
{

View File

@ -162,10 +162,9 @@ void AINodeStorage::calculateNeighbours(
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper)
{
std::vector<int3> accessibleNeighbourTiles;
NeighbourTilesVector accessibleNeighbourTiles;
result.clear();
accessibleNeighbourTiles.reserve(8);
pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);

View File

@ -85,6 +85,9 @@ void BonusList::stackBonuses()
int BonusList::totalValue() const
{
if (bonuses.empty())
return 0;
struct BonusCollection
{
int base = 0;
@ -96,63 +99,65 @@ int BonusList::totalValue() const
int indepMax = std::numeric_limits<int>::min();
};
auto percent = [](int64_t base, int64_t percent) -> int {
return static_cast<int>(std::clamp<int64_t>((base * (100 + percent)) / 100, std::numeric_limits<int>::min(), std::numeric_limits<int>::max()));
auto percent = [](int base, int percent) -> int {
return (static_cast<int64_t>(base) * (100 + percent)) / 100;
};
std::array <BonusCollection, vstd::to_underlying(BonusSource::NUM_BONUS_SOURCE)> sources = {};
BonusCollection any;
BonusCollection accumulated;
bool hasIndepMax = false;
bool hasIndepMin = false;
std::array<int, vstd::to_underlying(BonusSource::NUM_BONUS_SOURCE)> percentToSource = {};
for(const auto & b : bonuses)
{
switch(b->valType)
{
case BonusValueType::BASE_NUMBER:
sources[vstd::to_underlying(b->source)].base += b->val;
break;
case BonusValueType::PERCENT_TO_ALL:
sources[vstd::to_underlying(b->source)].percentToAll += b->val;
break;
case BonusValueType::PERCENT_TO_BASE:
sources[vstd::to_underlying(b->source)].percentToBase += b->val;
break;
case BonusValueType::PERCENT_TO_SOURCE:
sources[vstd::to_underlying(b->source)].percentToSource += b->val;
break;
percentToSource[vstd::to_underlying(b->source)] += b->val;
break;
case BonusValueType::PERCENT_TO_TARGET_TYPE:
sources[vstd::to_underlying(b->targetSourceType)].percentToSource += b->val;
break;
case BonusValueType::ADDITIVE_VALUE:
sources[vstd::to_underlying(b->source)].additive += b->val;
break;
case BonusValueType::INDEPENDENT_MAX:
hasIndepMax = true;
vstd::amax(sources[vstd::to_underlying(b->source)].indepMax, b->val);
break;
case BonusValueType::INDEPENDENT_MIN:
hasIndepMin = true;
vstd::amin(sources[vstd::to_underlying(b->source)].indepMin, b->val);
percentToSource[vstd::to_underlying(b->targetSourceType)] += b->val;
break;
}
}
for(const auto & src : sources)
{
any.base += percent(src.base, src.percentToSource);
any.percentToBase += percent(src.percentToBase, src.percentToSource);
any.percentToAll += percent(src.percentToAll, src.percentToSource);
any.additive += percent(src.additive, src.percentToSource);
if(hasIndepMin)
vstd::amin(any.indepMin, percent(src.indepMin, src.percentToSource));
if(hasIndepMax)
vstd::amax(any.indepMax, percent(src.indepMax, src.percentToSource));
}
any.base = percent(any.base, any.percentToBase);
any.base += any.additive;
auto valFirst = percent(any.base ,any.percentToAll);
if(hasIndepMin && hasIndepMax && any.indepMin < any.indepMax)
any.indepMax = any.indepMin;
for(const auto & b : bonuses)
{
int sourceIndex = vstd::to_underlying(b->source);
int valModified = percent(b->val, percentToSource[sourceIndex]);
switch(b->valType)
{
case BonusValueType::BASE_NUMBER:
accumulated.base += valModified;
break;
case BonusValueType::PERCENT_TO_ALL:
accumulated.percentToAll += valModified;
break;
case BonusValueType::PERCENT_TO_BASE:
accumulated.percentToBase += valModified;
break;
case BonusValueType::ADDITIVE_VALUE:
accumulated.additive += valModified;
break;
case BonusValueType::INDEPENDENT_MAX:
hasIndepMax = true;
vstd::amax(accumulated.indepMax, valModified);
break;
case BonusValueType::INDEPENDENT_MIN:
hasIndepMin = true;
vstd::amin(accumulated.indepMin, valModified);
break;
}
}
accumulated.base = percent(accumulated.base, accumulated.percentToBase);
accumulated.base += accumulated.additive;
auto valFirst = percent(accumulated.base ,accumulated.percentToAll);
if(hasIndepMin && hasIndepMax && accumulated.indepMin < accumulated.indepMax)
accumulated.indepMax = accumulated.indepMin;
const int notIndepBonuses = static_cast<int>(std::count_if(bonuses.cbegin(), bonuses.cend(), [](const std::shared_ptr<Bonus>& b)
{
@ -160,9 +165,9 @@ int BonusList::totalValue() const
}));
if(notIndepBonuses)
return std::clamp(valFirst, any.indepMax, any.indepMin);
return std::clamp(valFirst, accumulated.indepMax, accumulated.indepMin);
return hasIndepMin ? any.indepMin : hasIndepMax ? any.indepMax : 0;
return hasIndepMin ? accumulated.indepMin : hasIndepMax ? accumulated.indepMax : 0;
}
std::shared_ptr<Bonus> BonusList::getFirst(const CSelector &select)

View File

@ -157,7 +157,6 @@ void CLogger::log(ELogLevel::ELogLevel level, const boost::format & fmt) const
ELogLevel::ELogLevel CLogger::getLevel() const
{
TLockGuard _(mx);
return level;
}

View File

@ -1231,7 +1231,7 @@ void RemoveObject::applyGs(CGameState *gs)
gs->map->instanceNames.erase(obj->instanceName);
gs->map->objects[objectID.getNum()].dellNull();
gs->map->calculateGuardingGreaturePositions();
gs->map->calculateGuardingGreaturePositions();//FIXME: excessive, update only affected tiles
}
static int getDir(const int3 & src, const int3 & dst)

View File

@ -49,7 +49,7 @@ bool CPathfinderHelper::canMoveFromNode(const PathNodeInfo & source) const
return true;
}
void CPathfinderHelper::calculateNeighbourTiles(std::vector<int3> & result, const PathNodeInfo & source) const
void CPathfinderHelper::calculateNeighbourTiles(NeighbourTilesVector & result, const PathNodeInfo & source) const
{
result.clear();
@ -239,9 +239,9 @@ void CPathfinder::calculatePaths()
logAi->trace("CPathfinder finished with %s iterations", std::to_string(counter));
}
std::vector<int3> CPathfinderHelper::getAllowedTeleportChannelExits(const TeleportChannelID & channelID) const
TeleporterTilesVector CPathfinderHelper::getAllowedTeleportChannelExits(const TeleportChannelID & channelID) const
{
std::vector<int3> allowedExits;
TeleporterTilesVector allowedExits;
for(const auto & objId : getTeleportChannelExits(channelID, hero->tempOwner))
{
@ -262,9 +262,9 @@ std::vector<int3> CPathfinderHelper::getAllowedTeleportChannelExits(const Telepo
return allowedExits;
}
std::vector<int3> CPathfinderHelper::getCastleGates(const PathNodeInfo & source) const
TeleporterTilesVector CPathfinderHelper::getCastleGates(const PathNodeInfo & source) const
{
std::vector<int3> allowedExits;
TeleporterTilesVector allowedExits;
auto towns = getPlayerState(hero->tempOwner)->towns;
for(const auto & town : towns)
@ -279,9 +279,9 @@ std::vector<int3> CPathfinderHelper::getCastleGates(const PathNodeInfo & source)
return allowedExits;
}
std::vector<int3> CPathfinderHelper::getTeleportExits(const PathNodeInfo & source) const
TeleporterTilesVector CPathfinderHelper::getTeleportExits(const PathNodeInfo & source) const
{
std::vector<int3> teleportationExits;
TeleporterTilesVector teleportationExits;
const auto * objTeleport = dynamic_cast<const CGTeleport *>(source.nodeObject);
if(isAllowedTeleportEntrance(objTeleport))
@ -578,7 +578,7 @@ int CPathfinderHelper::getMaxMovePoints(const EPathfindingLayer & layer) const
void CPathfinderHelper::getNeighbours(
const TerrainTile & srcTile,
const int3 & srcCoord,
std::vector<int3> & vec,
NeighbourTilesVector & vec,
const boost::logic::tribool & onLand,
const bool limitCoastSailing) const
{
@ -702,8 +702,8 @@ int CPathfinderHelper::getMovementCost(
constexpr auto maxCostOfOneStep = static_cast<int>(175 * M_SQRT2); // diagonal move on Swamp - 247 MP
if(checkLast && left > 0 && left <= maxCostOfOneStep) //it might be the last tile - if no further move possible we take all move points
{
std::vector<int3> vec;
vec.reserve(8); //optimization
NeighbourTilesVector vec;
getNeighbours(*dt, dst, vec, ct->terType->isLand(), true);
for(const auto & elem : vec)
{

View File

@ -13,12 +13,23 @@
#include "../IGameCallback.h"
#include "../bonuses/BonusEnum.h"
#include <boost/container/static_vector.hpp>
#include <boost/container/small_vector.hpp>
VCMI_LIB_NAMESPACE_BEGIN
class CGWhirlpool;
struct TurnInfo;
struct PathfinderOptions;
// Optimized storage - tile can have 0-8 neighbour tiles
// static_vector uses fixed, preallocated storage (capacity) and dynamic size
// this avoid dynamic allocations on huge number of neighbour list queries
using NeighbourTilesVector = boost::container::static_vector<int3, 8>;
// Optimized storage to minimize dynamic allocations - most of teleporters have only one exit, but some may have more (premade maps, castle gates)
using TeleporterTilesVector = boost::container::small_vector<int3, 4>;
class DLL_LINKAGE CPathfinder
{
public:
@ -87,22 +98,22 @@ public:
bool hasBonusOfType(BonusType type) const;
int getMaxMovePoints(const EPathfindingLayer & layer) const;
std::vector<int3> getCastleGates(const PathNodeInfo & source) const;
TeleporterTilesVector getCastleGates(const PathNodeInfo & source) const;
bool isAllowedTeleportEntrance(const CGTeleport * obj) const;
std::vector<int3> getAllowedTeleportChannelExits(const TeleportChannelID & channelID) const;
TeleporterTilesVector getAllowedTeleportChannelExits(const TeleportChannelID & channelID) const;
bool addTeleportTwoWay(const CGTeleport * obj) const;
bool addTeleportOneWay(const CGTeleport * obj) const;
bool addTeleportOneWayRandom(const CGTeleport * obj) const;
bool addTeleportWhirlpool(const CGWhirlpool * obj) const;
bool canMoveBetween(const int3 & a, const int3 & b) const; //checks only for visitable objects that may make moving between tiles impossible, not other conditions (like tiles itself accessibility)
void calculateNeighbourTiles(std::vector<int3> & result, const PathNodeInfo & source) const;
std::vector<int3> getTeleportExits(const PathNodeInfo & source) const;
void calculateNeighbourTiles(NeighbourTilesVector & result, const PathNodeInfo & source) const;
TeleporterTilesVector getTeleportExits(const PathNodeInfo & source) const;
void getNeighbours(
const TerrainTile & srcTile,
const int3 & srcCoord,
std::vector<int3> & vec,
NeighbourTilesVector & vec,
const boost::logic::tribool & onLand,
const bool limitCoastSailing) const;

View File

@ -40,7 +40,7 @@ void NodeStorage::initialize(const PathfinderOptions & options, const CGameState
{
for(pos.y=0; pos.y < sizes.y; ++pos.y)
{
const TerrainTile tile = gs->map->getTile(pos);
const TerrainTile & tile = gs->map->getTile(pos);
if(tile.terType->isWater())
{
resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility<ELayer::SAIL>(pos, tile, fow, player, gs));
@ -67,10 +67,9 @@ void NodeStorage::calculateNeighbours(
const PathfinderConfig * pathfinderConfig,
const CPathfinderHelper * pathfinderHelper)
{
std::vector<int3> accessibleNeighbourTiles;
NeighbourTilesVector accessibleNeighbourTiles;
result.clear();
accessibleNeighbourTiles.reserve(8);
pathfinderHelper->calculateNeighbourTiles(accessibleNeighbourTiles, source);