diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index eac276ec5..66edcd0a3 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -73,13 +73,25 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector } 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) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 893e15633..a06070434 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -27,8 +27,8 @@ namespace NKAI { namespace AIPathfinding { - const int BUCKET_COUNT = 5; - const int BUCKET_SIZE = 3; + const int BUCKET_COUNT = 3; + const int BUCKET_SIZE = 5; const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE; const int CHAIN_MAX_DEPTH = 4; } diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp index 1efebafca..7327ed0e5 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp @@ -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(questInfo.obj)->checkQuest(node->actor->hero); + return dynamic_cast(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 diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.h b/AI/Nullkiller/Pathfinding/Actions/QuestAction.h index 52939e5f7..da0ef66c9 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/QuestAction.h @@ -30,6 +30,8 @@ namespace AIPathfinding bool canAct(const AIPathNode * node) const override; + bool canAct(const CGHeroInstance * hero) const; + Goals::TSubgoal decompose(const CGHeroInstance * hero) const override; void execute(const CGHeroInstance * hero) const override; diff --git a/AI/Nullkiller/Pathfinding/ObjectGraph.cpp b/AI/Nullkiller/Pathfinding/ObjectGraph.cpp index 5cddf022b..33851e919 100644 --- a/AI/Nullkiller/Pathfinding/ObjectGraph.cpp +++ b/AI/Nullkiller/Pathfinding/ObjectGraph.cpp @@ -15,10 +15,18 @@ #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: @@ -34,10 +42,14 @@ private: 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) + if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT) { addObjectActor(obj); } @@ -47,7 +59,70 @@ public: { 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; @@ -59,11 +134,31 @@ public: void calculateConnections(const int3 & pos) { - auto guarded = ai->cb->getGuardingCreaturePosition(pos).valid(); + 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()); + } + } + } + }); - if(guarded) return; + } + auto guardPos = ai->cb->getGuardingCreaturePosition(pos); auto paths = ai->pathfinder->getPathInfo(pos); for(AIPath & path1 : paths) @@ -73,25 +168,32 @@ public: 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(obj1->visitablePos()); - auto tile2 = cb->getTile(obj2->visitablePos()); + auto tile1 = cb->getTile(pos1); + auto tile2 = cb->getTile(pos2); if(tile2->isWater() && !tile1->isWater()) { - auto linkTile = cb->getTile(pos); + if(!cb->getTile(pos)->isWater()) + continue; - if(!linkTile->isWater() || obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD) + if(obj1 && (obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD)) continue; } - auto danger = ai->pathfinder->getStorage()->evaluateDanger(obj2->visitablePos(), path1.targetHero, true); + auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true); auto updated = target->tryAddConnection( - obj1->visitablePos(), - obj2->visitablePos(), + pos1, + pos2, path1.movementCost() + path2.movementCost(), danger); @@ -99,8 +201,8 @@ public: { logAi->trace( "Connected %s[%s] -> %s[%s] through [%s], cost %2f", - obj1->getObjectName(), obj1->visitablePos().toString(), - obj2->getObjectName(), obj2->visitablePos().toString(), + obj1 ? obj1->getObjectName() : "J", pos1.toString(), + obj2 ? obj2->getObjectName() : "J", pos2.toString(), pos.toString(), path1.movementCost() + path2.movementCost()); } @@ -108,7 +210,49 @@ public: } } -private: + 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> connectionsToRemove; + + for(auto & actor : temporaryActorHeroes) + { + auto pos = actor->visitablePos(); + auto & currentNode = target->getNode(pos); + + target->iterateConnections(pos, [this, &pos, &connectionsToRemove, ¤tNode](int3 n1, ObjectLink o1) + { + target->iterateConnections(n1, [&pos, &o1, ¤tNode, &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(obj->cb)).get(); @@ -126,11 +270,77 @@ private: objectActor->boat = temporaryBoats.emplace_back(std::make_unique(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(internalCb)).get(); + + CRandomGenerator rng; + + objectActor->setOwner(ai->playerID); // lets avoid having multiple colors + objectActor->initHero(rng, static_cast(0)); + objectActor->pos = objectActor->convertFromVisitablePos(visitablePos); + objectActor->initObj(rng); + + if(cb->getTile(visitablePos)->isWater()) + { + objectActor->boat = temporaryBoats.emplace_back(std::make_unique(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 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( @@ -142,23 +352,24 @@ bool ObjectGraph::tryAddConnection( 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); - foreach_tile_pos(cb.get(), [this, &calculator](const CPlayerSpecificInfoCallback * cb, const int3 & pos) - { - if(nodes.find(pos) != nodes.end()) - return; - - calculator.calculateConnections(pos); - }); + calculator.setGraphObjects(); + calculator.calculateConnections(); + calculator.addMinimalDistanceJunctions(); + calculator.calculateConnections(); if(NKAI_GRAPH_TRACE_LEVEL >= 1) dumpToLog("graph"); - } void ObjectGraph::addObject(const CGObjectInstance * obj) @@ -166,6 +377,11 @@ 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; @@ -198,6 +414,9 @@ void ObjectGraph::connectHeroes(const Nullkiller * ai) for(AIPath & path : paths) { + if(path.getFirstBlockedAction()) + continue; + auto heroPos = path.targetHero->visitablePos(); nodes[pos].connections[heroPos].update( @@ -259,20 +478,55 @@ void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkil GraphPathNodePointer pos = pq.top(); pq.pop(); - auto & node = getNode(pos); + auto & node = getOrCreateNode(pos); + std::shared_ptr 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(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(questInfo); + + if(!questAction->canAct(targetHero)) + { + transitionAction = questAction; + } + } + } node.isInQueue = false; - graph.iterateConnections(pos.coord, [&](int3 target, ObjectLink o) + graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, ObjectLink o) { - auto targetNodeType = o.danger ? GrapthPathNodeType::BATTLE : pos.nodeType; + auto targetNodeType = o.danger || transitionAction ? GrapthPathNodeType::BATTLE : pos.nodeType; auto targetPointer = GraphPathNodePointer(target, targetNodeType); - auto & targetNode = getNode(targetPointer); + auto & targetNode = getOrCreateNode(targetPointer); if(targetNode.tryUpdate(pos, node, o)) { - if(graph.getNode(target).objTypeID == Obj::HERO) - return; + 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) { @@ -321,11 +575,6 @@ bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathN if(newCost < cost) { - if(nodeType < pos.nodeType) - { - logAi->error("Linking error"); - } - previous = pos; danger = prev.danger + link.danger; cost = newCost; @@ -348,7 +597,7 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe if(!node.reachable()) continue; - std::vector tilesToPass; + std::vector tilesToPass; uint64_t danger = node.danger; float cost = node.cost; @@ -372,7 +621,7 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe vstd::amax(danger, currentNode.danger); vstd::amax(cost, currentNode.cost); - tilesToPass.push_back(current.coord); + tilesToPass.push_back(current); if(currentNode.cost < 2.0f) break; @@ -383,7 +632,7 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe if(tilesToPass.empty()) continue; - auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back()); + auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord); for(auto & path : entryPaths) { @@ -394,12 +643,13 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe { AIPathNodeInfo n; - n.coord = *graphTile; + n.coord = graphTile->coord; n.cost = cost; n.turns = static_cast(cost) + 1; // just in case lets select worst scenario n.danger = danger; n.targetHero = hero; - n.parentIndex = 0; + n.parentIndex = -1; + n.specialAction = getNode(*graphTile).specialAction; for(auto & node : path.nodes) { diff --git a/AI/Nullkiller/Pathfinding/ObjectGraph.h b/AI/Nullkiller/Pathfinding/ObjectGraph.h index 4407dfc93..c33ebbbdf 100644 --- a/AI/Nullkiller/Pathfinding/ObjectGraph.h +++ b/AI/Nullkiller/Pathfinding/ObjectGraph.h @@ -50,6 +50,13 @@ struct ObjectNode objID = obj->id; objTypeID = obj->ID; } + + void initJunction() + { + objectExists = false; + objID = ObjectInstanceID(); + objTypeID = Obj(); + } }; class ObjectGraph @@ -59,8 +66,11 @@ class ObjectGraph 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 @@ -77,7 +87,10 @@ public: return nodes.at(tile); } - bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger); + bool hasNodeAt(const int3 & tile) const + { + return vstd::contains(nodes, tile); + } }; struct GraphPathNode; @@ -131,6 +144,8 @@ struct GraphPathNode GraphPathNodePointer previous; float cost = BAD_COST; uint64_t danger = 0; + const CGObjectInstance * obj = nullptr; + std::shared_ptr specialAction; using TFibHeap = boost::heap::fibonacci_heap>; @@ -157,7 +172,7 @@ public: void dumpToLog() const; private: - GraphPathNode & getNode(const GraphPathNodePointer & pos) + GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos) { auto & node = pathNodes[pos.coord][pos.nodeType]; @@ -165,6 +180,13 @@ private: return node; } + + const GraphPathNode & getNode(const GraphPathNodePointer & pos) const + { + auto & node = pathNodes.at(pos.coord)[pos.nodeType]; + + return node; + } }; } diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index 9aa5719ae..1d2e2644d 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -42,15 +42,34 @@ 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(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; + } #if NKAI_PATHFINDER_TRACE_LEVEL >= 2 logAi->trace( diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp index 59368e9c4..307cd62e6 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp @@ -49,7 +49,7 @@ namespace AIPathfinding return; // when actor represents moster graph node, we need to let him escape monster - if(!destination.guarded && cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition) + if(cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition) return; } diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 008d28fc5..23ef811b1 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -71,6 +71,8 @@ public: virtual void drawLine(int3 start, int3 end) override { + const Point offset = Point(30, 30); + auto level = model->getLevel(); if(start.z != level || end.z != level) @@ -83,6 +85,9 @@ public: pStart.x += 3; pEnd.x -= 3; + pStart += offset; + pEnd += offset; + if(viewPort.isInside(pStart) && viewPort.isInside(pEnd)) { target.drawLine(pStart, pEnd, ColorRGBA(255, 255, 0), ColorRGBA(255, 0, 0));