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

NKAI: improve boat handling by object graph, a set of fixes

This commit is contained in:
Andrii Danylchenko 2024-03-28 13:39:15 +02:00
parent adfcb650e2
commit 30d9daf62c
16 changed files with 234 additions and 51 deletions

View File

@ -329,14 +329,24 @@ void ObjectClusterizer::clusterizeObject(
if(ai->heroManager->getHeroRole(path.targetHero) == HeroRole::SCOUT)
{
if(path.movementCost() > 2.0f)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Path is too far %f", path.movementCost());
#endif
continue;
}
}
else if(path.movementCost() > 4.0f)
else if(path.movementCost() > 4.0f && obj->ID != Obj::TOWN)
{
auto strategicalValue = valueEvaluator.getStrategicalValue(obj);
if(strategicalValue < 0.5f)
if(strategicalValue < 0.3f)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Object value is too low %f", strategicalValue);
#endif
continue;
}
}
if(!shouldVisit(ai, path.targetHero, obj))

View File

@ -49,6 +49,9 @@ Goals::TGoalVec StayAtTownBehavior::decompose() const
if(town->visitingHero && town->visitingHero.get() != path.targetHero)
continue;
if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
continue;
if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
{
if(path.targetHero->mana == path.targetHero->manaLimit())

View File

@ -67,40 +67,40 @@ void ExecuteHeroChain::accept(AIGateway * ai)
for(int i = chainPath.nodes.size() - 1; i >= 0; i--)
{
auto & node = chainPath.nodes[i];
auto * node = &chainPath.nodes[i];
const CGHeroInstance * hero = node.targetHero;
const CGHeroInstance * hero = node->targetHero;
HeroPtr heroPtr = hero;
if(node.parentIndex >= i)
if(node->parentIndex >= i)
{
logAi->error("Invalid parentIndex while executing node " + node.coord.toString());
logAi->error("Invalid parentIndex while executing node " + node->coord.toString());
}
if(vstd::contains(blockedIndexes, i))
{
blockedIndexes.insert(node.parentIndex);
blockedIndexes.insert(node->parentIndex);
ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
continue;
}
logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->getNameTranslated(), node.coord.toString());
logAi->debug("Executing chain node %d. Moving hero %s to %s", i, hero->getNameTranslated(), node->coord.toString());
try
{
if(hero->movementPointsRemaining() > 0)
{
ai->nullkiller->setActive(hero, node.coord);
ai->nullkiller->setActive(hero, node->coord);
if(node.specialAction)
if(node->specialAction)
{
if(node.actionIsBlocked)
if(node->actionIsBlocked)
{
throw cannotFulfillGoalException("Path is nondeterministic.");
}
node.specialAction->execute(hero);
node->specialAction->execute(hero);
if(!heroPtr.validAndSet())
{
@ -109,10 +109,34 @@ void ExecuteHeroChain::accept(AIGateway * ai)
return;
}
}
if(node.turns == 0 && node.coord != hero->visitablePos())
else if(i > 0 && ai->nullkiller->settings->isObjectGraphAllowed())
{
auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node.coord);
auto chainMask = i < chainPath.nodes.size() - 1 ? chainPath.nodes[i + 1].chainMask : node->chainMask;
for(auto j = i - 1; j >= 0; j--)
{
auto & nextNode = chainPath.nodes[j];
if(nextNode.specialAction || nextNode.chainMask != chainMask)
break;
auto targetNode = cb->getPathsInfo(hero)->getPathInfo(nextNode.coord);
if(!targetNode->reachable()
|| targetNode->getCost() > nextNode.cost)
break;
i = j;
node = &nextNode;
if(targetNode->action == EPathNodeAction::BATTLE || targetNode->action == EPathNodeAction::TELEPORT_BATTLE)
break;
}
}
if(node->turns == 0 && node->coord != hero->visitablePos())
{
auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node->coord);
if(targetNode->accessible == EPathAccessibility::NOT_SET
|| targetNode->accessible == EPathAccessibility::BLOCKED
@ -122,7 +146,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
logAi->error(
"Unable to complete chain. Expected hero %s to arrive to %s in 0 turns but he cannot do this",
hero->getNameTranslated(),
node.coord.toString());
node->coord.toString());
return;
}
@ -132,7 +156,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
{
try
{
if(moveHeroToTile(hero, node.coord))
if(moveHeroToTile(hero, node->coord))
{
continue;
}
@ -149,11 +173,11 @@ void ExecuteHeroChain::accept(AIGateway * ai)
if(hero->movementPointsRemaining() > 0)
{
CGPath path;
bool isOk = cb->getPathsInfo(hero)->getPath(path, node.coord);
bool isOk = cb->getPathsInfo(hero)->getPath(path, node->coord);
if(isOk && path.nodes.back().turns > 0)
{
logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node.coord.toString());
logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node->coord.toString());
ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
return;
@ -165,15 +189,15 @@ void ExecuteHeroChain::accept(AIGateway * ai)
}
}
if(node.coord == hero->visitablePos())
if(node->coord == hero->visitablePos())
continue;
if(node.turns == 0)
if(node->turns == 0)
{
logAi->error(
"Unable to complete chain. Expected hero %s to arive to %s but he is at %s",
hero->getNameTranslated(),
node.coord.toString(),
node->coord.toString(),
hero->visitablePos().toString());
return;
@ -181,13 +205,13 @@ void ExecuteHeroChain::accept(AIGateway * ai)
// no exception means we were not able to reach the tile
ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
blockedIndexes.insert(node.parentIndex);
blockedIndexes.insert(node->parentIndex);
}
catch(const goalFulfilledException &)
{
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

@ -529,7 +529,7 @@ bool AINodeStorage::calculateHeroChain()
tbb::parallel_for(tbb::blocked_range<size_t>(0, data.size()), [&](const tbb::blocked_range<size_t>& r)
{
//auto r = blocked_range<size_t>(0, data.size());
HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
task.execute(r);
@ -543,7 +543,7 @@ bool AINodeStorage::calculateHeroChain()
else
{
auto r = tbb::blocked_range<size_t>(0, data.size());
HeroChainCalculationTask task(*this, nodes, data, chainMask, heroChainTurn);
HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
task.execute(r);
task.flushResult(heroChain);

View File

@ -28,7 +28,7 @@ namespace AIPathfinding
bool allowBypassObjects)
{
std::vector<std::shared_ptr<IPathfindingRule>> rules = {
std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage),
std::make_shared<AILayerTransitionRule>(cb, ai, nodeStorage, allowBypassObjects),
std::make_shared<DestinationActionRule>(),
std::make_shared<AIMovementToDestinationRule>(nodeStorage, allowBypassObjects),
std::make_shared<MovementCostRule>(),

View File

@ -37,10 +37,8 @@ namespace AIPathfinding
return Goals::sptr(Goals::Invalid());
}
bool BuildBoatAction::canAct(const AIPathNode * source) const
bool BuildBoatAction::canAct(const CGHeroInstance * hero, const TResources & reservedResources) const
{
auto hero = source->actor->hero;
if(cb->getPlayerRelations(hero->tempOwner, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
{
#if NKAI_TRACE_LEVEL > 1
@ -53,7 +51,7 @@ namespace AIPathfinding
shipyard->getBoatCost(boatCost);
if(!cb->getResourceAmount().canAfford(source->actor->armyCost + boatCost))
if(!cb->getResourceAmount().canAfford(reservedResources + boatCost))
{
#if NKAI_TRACE_LEVEL > 1
logAi->trace("Can not build a boat. Not enough resources.");
@ -65,6 +63,18 @@ namespace AIPathfinding
return true;
}
bool BuildBoatAction::canAct(const AIPathNode * source) const
{
return canAct(source->actor->hero, source->actor->armyCost);
}
bool BuildBoatAction::canAct(const AIPathNodeInfo & source) const
{
TResources res;
return canAct(source.targetHero, res);
}
const CGObjectInstance * BuildBoatAction::targetObject() const
{
return dynamic_cast<const CGObjectInstance*>(shipyard);
@ -75,6 +85,11 @@ namespace AIPathfinding
return sourceActor->resourceActor;
}
std::shared_ptr<SpecialAction> BuildBoatActionFactory::create(const Nullkiller * ai)
{
return std::make_shared<BuildBoatAction>(ai->cb.get(), dynamic_cast<const IShipyard * >(ai->cb->getObj(shipyard)));
}
void SummonBoatAction::execute(const CGHeroInstance * hero) const
{
Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai);

View File

@ -57,6 +57,8 @@ namespace AIPathfinding
}
bool canAct(const AIPathNode * source) const override;
bool canAct(const AIPathNodeInfo & source) const override;
bool canAct(const CGHeroInstance * hero, const TResources & reservedResources) const;
void execute(const CGHeroInstance * hero) const override;
@ -68,6 +70,19 @@ namespace AIPathfinding
const CGObjectInstance * targetObject() const override;
};
class BuildBoatActionFactory : public ISpecialActionFactory
{
ObjectInstanceID shipyard;
public:
BuildBoatActionFactory(ObjectInstanceID shipyard)
:shipyard(shipyard)
{
}
std::shared_ptr<SpecialAction> create(const Nullkiller * ai) override;
};
}
}

View File

@ -23,6 +23,11 @@ namespace AIPathfinding
return canAct(node->actor->hero);
}
bool QuestAction::canAct(const AIPathNodeInfo & node) const
{
return canAct(node.targetHero);
}
bool QuestAction::canAct(const CGHeroInstance * hero) const
{
if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)

View File

@ -29,7 +29,7 @@ namespace AIPathfinding
}
bool canAct(const AIPathNode * node) const override;
bool canAct(const AIPathNodeInfo & node) const override;
bool canAct(const CGHeroInstance * hero) const;
Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;

View File

@ -22,6 +22,7 @@ namespace NKAI
{
struct AIPathNode;
struct AIPathNodeInfo;
class ChainActor;
class SpecialAction
@ -34,6 +35,11 @@ public:
return true;
}
virtual bool canAct(const AIPathNodeInfo & source) const
{
return true;
}
virtual Goals::TSubgoal decompose(const CGHeroInstance * hero) const;
virtual void execute(const CGHeroInstance * hero) const;
@ -89,4 +95,10 @@ public:
const AIPathNode * srcNode) const override;
};
class ISpecialActionFactory
{
public:
virtual std::shared_ptr<SpecialAction> create(const Nullkiller * ai) = 0;
};
}

View File

@ -17,6 +17,7 @@
#include "../../../lib/logging/VisualLogger.h"
#include "Actions/QuestAction.h"
#include "../pforeach.h"
#include "Actions/BoatActions.h"
namespace NKAI
{
@ -121,7 +122,7 @@ public:
ConnectionCostInfo currentCost = getConnectionsCost(paths);
if(currentCost.connectionsCount <= 2)
if(currentCost.connectionsCount <= 1 || currentCost.connectionsCount == 2 && currentCost.totalCost < 3.0f)
return;
float neighborCost = getNeighborConnectionsCost(pos, paths);
@ -170,7 +171,7 @@ private:
auto obj = ai->cb->getTopObj(pos);
if(obj && obj->ID == Obj::BOAT)
if((obj && obj->ID == Obj::BOAT) || target->isVirtualBoat(pos))
{
ai->pathfinder->calculatePathInfo(pathCache, pos);
@ -230,9 +231,9 @@ private:
if(!cb->getTile(pos)->isWater())
continue;
auto startingObjIsBoatOrShipyard = obj1 && (obj1->ID == Obj::BOAT || obj1->ID == Obj::SHIPYARD);
auto startingObjIsBoat = (obj1 && obj1->ID == Obj::BOAT) || target->isVirtualBoat(pos1);
if(!startingObjIsBoatOrShipyard)
if(!startingObjIsBoat)
continue;
}
@ -320,12 +321,22 @@ private:
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = obj;
actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD || obj->ID == Obj::BOAT ? HeroRole::MAIN : HeroRole::SCOUT;
actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::BOAT ? HeroRole::MAIN : HeroRole::SCOUT;
target->addObject(obj);
auto shipyard = dynamic_cast<const IShipyard *>(obj);
if(shipyard && shipyard->bestLocation().valid())
{
int3 virtualBoat = shipyard->bestLocation();
addJunctionActor(virtualBoat, true);
target->addVirtualBoat(virtualBoat, obj);
}
}
void addJunctionActor(const int3 & visitablePos)
void addJunctionActor(const int3 & visitablePos, bool isVirtualBoat = false)
{
std::lock_guard<std::mutex> lock(syncLock);
@ -339,7 +350,7 @@ private:
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
if(ai->cb->getTile(visitablePos)->isWater())
if(isVirtualBoat || ai->cb->getTile(visitablePos)->isWater())
{
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
@ -347,7 +358,7 @@ private:
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = nullptr;
actors[objectActor] = HeroRole::SCOUT;
actors[objectActor] = isVirtualBoat ? HeroRole::MAIN : HeroRole::SCOUT;
target->registerJunction(visitablePos);
}
@ -397,7 +408,15 @@ bool ObjectGraph::tryAddConnection(
float cost,
uint64_t danger)
{
return nodes[from].connections[to].update(cost, danger);
auto result = nodes[from].connections[to].update(cost, danger);
auto & connection = nodes[from].connections[to];
if(result && isVirtualBoat(to) && !connection.specialAction)
{
connection.specialAction = std::make_shared<AIPathfinding::BuildBoatActionFactory>(virtualBoats[to]);
}
return result;
}
void ObjectGraph::removeConnection(const int3 & from, const int3 & to)
@ -422,19 +441,30 @@ void ObjectGraph::updateGraph(const Nullkiller * ai)
void ObjectGraph::addObject(const CGObjectInstance * obj)
{
nodes[obj->visitablePos()].init(obj);
if(!hasNodeAt(obj->visitablePos()))
nodes[obj->visitablePos()].init(obj);
}
void ObjectGraph::addVirtualBoat(const int3 & pos, const CGObjectInstance * shipyard)
{
if(!isVirtualBoat(pos))
{
virtualBoats[pos] = shipyard->id;
}
}
void ObjectGraph::registerJunction(const int3 & pos)
{
nodes[pos].initJunction();
if(!hasNodeAt(pos))
nodes[pos].initJunction();
}
void ObjectGraph::removeObject(const CGObjectInstance * obj)
{
nodes[obj->visitablePos()].objectExists = false;
if(obj->ID == Obj::BOAT)
if(obj->ID == Obj::BOAT && !isVirtualBoat(obj->visitablePos()))
{
vstd::erase_if(nodes[obj->visitablePos()].connections, [&](const std::pair<int3, ObjectLink> & link) -> bool
{
@ -512,6 +542,27 @@ GraphPaths::GraphPaths()
{
}
std::shared_ptr<SpecialAction> getCompositeAction(
const Nullkiller * ai,
std::shared_ptr<ISpecialActionFactory> linkActionFactory,
std::shared_ptr<SpecialAction> transitionAction)
{
if(!linkActionFactory)
return transitionAction;
auto linkAction = linkActionFactory->create(ai);
if(!transitionAction)
return linkAction;
std::vector<std::shared_ptr<const SpecialAction>> actionsArray = {
transitionAction,
linkAction
};
return std::make_shared<CompositeAction>(actionsArray);
}
void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkiller * ai)
{
graph.copyFrom(*ai->baseGraph);
@ -561,15 +612,16 @@ void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkil
node.isInQueue = false;
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, ObjectLink o)
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, const ObjectLink & o)
{
auto targetNodeType = o.danger || transitionAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
auto compositeAction = getCompositeAction(ai, o.specialAction, transitionAction);
auto targetNodeType = o.danger || compositeAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
auto targetPointer = GraphPathNodePointer(target, targetNodeType);
auto & targetNode = getOrCreateNode(targetPointer);
if(targetNode.tryUpdate(pos, node, o))
{
targetNode.specialAction = transitionAction;
targetNode.specialAction = compositeAction;
auto targetGraphNode = graph.getNode(target);
@ -704,6 +756,11 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
n.parentIndex = -1;
n.specialAction = getNode(*graphTile).specialAction;
if(n.specialAction)
{
n.actionIsBlocked = !n.specialAction->canAct(n);
}
for(auto & node : path.nodes)
{
node.parentIndex++;
@ -806,7 +863,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
}
}
if(!path.nodes.empty())
if(path.nodes.size() > 1)
continue;
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
@ -820,6 +877,11 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
n.specialAction = node.specialAction;
n.parentIndex = path.nodes.size();
if(n.specialAction)
{
n.actionIsBlocked = !n.specialAction->canAct(n);
}
auto blocker = ai->objectClusterizer->getBlocker(n);
if(!blocker)

View File

@ -22,6 +22,7 @@ struct ObjectLink
{
float cost = 100000; // some big number
uint64_t danger = 0;
std::shared_ptr<ISpecialActionFactory> specialAction;
bool update(float newCost, uint64_t newDanger)
{
@ -62,20 +63,33 @@ struct ObjectNode
class ObjectGraph
{
std::unordered_map<int3, ObjectNode> nodes;
std::unordered_map<int3, ObjectInstanceID> virtualBoats;
public:
ObjectGraph()
:nodes(), virtualBoats()
{
}
void updateGraph(const Nullkiller * ai);
void addObject(const CGObjectInstance * obj);
void registerJunction(const int3 & pos);
void addVirtualBoat(const int3 & pos, const CGObjectInstance * shipyard);
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;
bool isVirtualBoat(const int3 & tile) const
{
return vstd::contains(virtualBoats, tile);
}
void copyFrom(const ObjectGraph & other)
{
nodes = other.nodes;
virtualBoats = other.virtualBoats;
}
template<typename Func>

View File

@ -17,8 +17,12 @@ namespace NKAI
{
namespace AIPathfinding
{
AILayerTransitionRule::AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage)
:cb(cb), ai(ai), nodeStorage(nodeStorage)
AILayerTransitionRule::AILayerTransitionRule(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowEmbark)
:cb(cb), ai(ai), nodeStorage(nodeStorage), allowEmbark(allowEmbark)
{
setup();
}

View File

@ -32,9 +32,14 @@ namespace AIPathfinding
std::map<const CGHeroInstance *, std::shared_ptr<const SummonBoatAction>> summonableVirtualBoats;
std::map<const CGHeroInstance *, std::shared_ptr<const WaterWalkingAction>> waterWalkingActions;
std::map<const CGHeroInstance *, std::shared_ptr<const AirWalkingAction>> airWalkingActions;
bool allowEmbark;
public:
AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage);
AILayerTransitionRule(
CPlayerSpecificInfoCallback * cb,
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowEmbark);
virtual void process(
const PathNodeInfo & source,

View File

@ -41,6 +41,16 @@ namespace AIPathfinding
return;
}
if(!allowBypassObjects
&& destination.action == EPathNodeAction::EMBARK
&& source.node->layer == EPathfindingLayer::LAND
&& destination.node->layer == EPathfindingLayer::SAIL)
{
destination.blocked = true;
return;
}
auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
if(blocker == BlockingReason::NONE)

View File

@ -396,7 +396,11 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
// In auto testing pack.mode we always close client if red pack.player won or lose
if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0))
{
logAi->info("Red player %s. Ending game.", pack.victoryLossCheckResult.victory() ? "won" : "lost");
handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not
}
}
void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack)