فهرست منبع

add pathfinderTurnStorageMisses to track how bucket size should be adjusted; modfied nk2ai-settings.json to simulate 3 clearer AI difficulty levels

Mircea TheHonestCTO 3 ماه پیش
والد
کامیت
9cdf74fb42

+ 4 - 0
AI/Nullkiller2/AIGateway.cpp

@@ -829,9 +829,13 @@ void AIGateway::makeTurn()
 
 	try
 	{
+		nullkiller->pathfinderTurnStorageMisses.store(0);
 		nullkiller->makeTurn();
 
 		// for debug purpose
+		if (nullkiller->pathfinderTurnStorageMisses.load() != 0)
+			logAi->warn("AINodeStorage had %d nodeAllocationFailures due to limited capacity", nullkiller->pathfinderTurnStorageMisses.load());
+
 		for (const auto *h : cc->getHeroesInfo())
 		{
 			if (h->movementPointsRemaining())

+ 2 - 2
AI/Nullkiller2/Engine/Nullkiller.cpp

@@ -461,7 +461,7 @@ void Nullkiller::makeTurn()
 				continue;
 			}
 
-			logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, selectedTask->toString(), selectedTask->priority);
+			logAi->info("Pass %d: Performing task (prioOfTask %d) %s with prio: %d", i, prioOfTask, selectedTask->toString(), selectedTask->priority);
 
 			if(HeroPtr heroPtr(selectedTask->getHero(), cc); selectedTask->getHero() && !heroPtr.isVerified(false))
 			{
@@ -514,7 +514,7 @@ bool Nullkiller::updateStateAndExecutePriorityPass(Goals::TGoalVec & tempResults
 
 		if(bestPrioPassTask->priority > 0)
 		{
-			logAi->info("Pass %d: Performing priorityPass %d task %s with prio: %d", passIndex, i, bestPrioPassTask->toString(), bestPrioPassTask->priority);
+			logAi->info("Pass %d: priorityPass %d: Performing task %s with prio: %d", passIndex, i, bestPrioPassTask->toString(), bestPrioPassTask->priority);
 
 			const bool isRecruitHeroGoal = dynamic_cast<RecruitHero*>(bestPrioPassTask.get()) != nullptr;
 			HeroPtr heroPtr(bestPrioPassTask->getHero(), cc);

+ 1 - 0
AI/Nullkiller2/Engine/Nullkiller.h

@@ -97,6 +97,7 @@ public:
 	std::unique_ptr<PriorityEvaluator> priorityEvaluator;
 	std::unique_ptr<SharedPool<PriorityEvaluator>> priorityEvaluators;
 	std::unique_ptr<AIPathfinder> pathfinder;
+	std::atomic_int32_t pathfinderTurnStorageMisses;
 	std::unique_ptr<HeroManager> heroManager;
 	std::unique_ptr<ArmyManager> armyManager;
 	std::unique_ptr<AIMemory> memory;

+ 42 - 24
AI/Nullkiller2/Pathfinding/AINodeStorage.cpp

@@ -104,7 +104,7 @@ int AINodeStorage::getBucketSize() const
 	return aiNk->settings->getPathfinderBucketSize();
 }
 
-AINodeStorage::AINodeStorage(const Nullkiller * aiNk, const int3 & Sizes)
+AINodeStorage::AINodeStorage(Nullkiller * aiNk, const int3 & Sizes)
 	: sizes(Sizes), aiNk(aiNk), nodes(Sizes, aiNk->settings->getPathfinderBucketSize() * aiNk->settings->getPathfinderBucketsCount())
 {
 	accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
@@ -182,9 +182,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 	const EPathfindingLayer layer, 
 	const ChainActor * actor)
 {
-	// TODO: Mircea: This might even be a bug because in other places it's just saying nodes.get(pos)
-	// It's probably just trying to limit storage per actor, but memory usage is the same and we might waste some if
-	// some actors have few chains
+	// Mircea: A bit more CPU to drop buckets but better memory usage to use all nodes, then we avoid unbalanced distribution
 	int bucketIndex = ((uintptr_t)actor + layer.getNum()) % aiNk->settings->getPathfinderBucketsCount();
 	int bucketOffset = bucketIndex * aiNk->settings->getPathfinderBucketSize();
 	auto chains = nodes.get(pos);
@@ -212,6 +210,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 		}
 	}
 
+	aiNk->pathfinderTurnStorageMisses.fetch_add(1);
 	return std::nullopt;
 }
 
@@ -227,17 +226,23 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 
 	std::vector<CGPathNode *> initialNodes;
 
-	for(auto actorPtr : actors)
+	for(const auto & actorPtr : actors)
 	{
-		ChainActor * actor = actorPtr.get();
+		const ChainActor * actor = actorPtr.get();
 
 		auto allocated = getOrCreateNode(actor->initialPosition, actor->layer, actor);
-
 		if(!allocated)
+		{
+#if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
+			logAi->trace(
+				"AINodeStorage::getInitialNodes Failed to allocate node at %s[%d]",
+				actor->initialPosition.toString(),
+				static_cast<int32_t>(actor->layer));
+#endif
 			continue;
+		}
 
 		AIPathNode * initialNode = allocated.value();
-
 		initialNode->pq = nullptr;
 		initialNode->turns = actor->initialTurn;
 		initialNode->moveRemains = actor->initialMovement;
@@ -381,7 +386,7 @@ void AINodeStorage::calculateNeighbours(
 		{
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
 			logAi->trace(
-				"Node %s rejected for %s, layer %d because of inaccessibility",
+				"AINodeStorage::calculateNeighbours Node %s rejected for %s, layer %d because of inaccessibility",
 				neighbour.toString(),
 				source.coord.toString(),
 				static_cast<int32_t>(layer));
@@ -390,12 +395,11 @@ void AINodeStorage::calculateNeighbours(
 		}
 
 		auto nextNode = getOrCreateNode(neighbour, layer, srcNode->actor);
-
 		if(!nextNode)
 		{
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
 			logAi->trace(
-				"Failed to allocate node at %s[%d]",
+				"AINodeStorage::calculateNeighbours Failed to allocate node at %s[%d]",
 				neighbour.toString(),
 				static_cast<int32_t>(layer));
 #endif
@@ -404,7 +408,7 @@ void AINodeStorage::calculateNeighbours(
 
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
 		logAi->trace(
-			"Node %s added to neighbors of %s, layer %d",
+			"AINodeStorage::calculateNeighbours Node %s added to neighbors of %s, layer %d",
 			neighbour.toString(),
 			source.coord.toString(),
 			static_cast<int32_t>(layer));
@@ -815,18 +819,20 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
 		if(!chainNodeOptional)
 		{
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
-			logAi->trace("Exchange at %s can not allocate node. Blocked.", carrier->coord.toString());
+			logAi->trace(
+				"HeroChainCalculationTask::addHeroChain Failed to allocate node at %s[%d]",
+				carrier->coord.toString(),
+				static_cast<int32_t>(carrier->layer));
 #endif
 			continue;
 		}
 
-		auto exchangeNode = chainNodeOptional.value();
-
+		auto * const exchangeNode = chainNodeOptional.value();
 		if(exchangeNode->action != EPathNodeAction::UNKNOWN)
 		{
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
 			logAi->trace(
-				"Skip exchange %s[%x] -> %s[%x] at %s because node is in use",
+				"HeroChainCalculationTask::addHeroChain Skip exchange %s[%x] -> %s[%x] at %s because node is in use",
 				other->actor->toString(),
 				other->actor->chainMask,
 				carrier->actor->toString(),
@@ -840,7 +846,7 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
 		{
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
 			logAi->trace(
-				"Skip exchange %s[%x] -> %s[%x] at %s because not effective enough. %f < %f",
+				"HeroChainCalculationTask::addHeroChain Skip exchange %s[%x] -> %s[%x] at %s because not effective enough. %f < %f",
 				other->actor->toString(),
 				other->actor->chainMask,
 				carrier->actor->toString(),
@@ -878,7 +884,7 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
 
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
 		logAi->trace(
-			"Chain accepted at %s %s -> %s, mask %x, cost %f, turn: %s, mp: %d, army %i", 
+			"HeroChainCalculationTask::addHeroChain Chain accepted at %s %s -> %s, mask %x, cost %f, turn: %s, mp: %d, army %i",
 			exchangeNode->coord.toString(), 
 			other->actor->toString(), 
 			exchangeNode->actor->toString(),
@@ -1053,9 +1059,16 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
 		for(auto & neighbour : accessibleExits)
 		{
 			std::optional<AIPathNode *> node = getOrCreateNode(neighbour, source.node->layer, srcNode->actor);
-			
 			if(!node)
+			{
+#if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
+				logAi->trace(
+					"AINodeStorage::calculateTeleportations Failed to allocate node at %s[%d]",
+					neighbour.toString(),
+					static_cast<int32_t>(source.node->layer));
+#endif
 				continue;
+			}
 
 			neighbours.push_back(node.value());
 		}
@@ -1128,18 +1141,23 @@ struct TownPortalFinder
 	std::optional<AIPathNode *> createTownPortalNode(const CGTownInstance * targetTown)
 	{
 		auto bestNode = getBestInitialNodeForTownPortal(targetTown);
-
 		if(!bestNode)
 			return std::nullopt;
 
-		auto nodeOptional = nodeStorage->getOrCreateNode(targetTown->visitablePos(), EPathfindingLayer::LAND, actor->castActor);
-
+		const auto nodeOptional = nodeStorage->getOrCreateNode(targetTown->visitablePos(), EPathfindingLayer::LAND, actor->castActor);
 		if(!nodeOptional)
+		{
+#if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
+			logAi->trace(
+				"createTownPortalNode Failed to allocate node at %s[%d]",
+				targetTown->visitablePos().toString(),
+				static_cast<int32_t>(EPathfindingLayer::LAND));
+#endif
 			return std::nullopt;
+		}
 
 		AIPathNode * node = nodeOptional.value();
 		float movementCost = (float)movementNeeded / (float)hero->movementPointsLimit(EPathfindingLayer::LAND);
-
 		movementCost += bestNode->getCost();
 
 		if(node->action == EPathNodeAction::UNKNOWN || node->getCost() > movementCost)
@@ -1215,7 +1233,7 @@ void AINodeStorage::calculateTownPortal(
 
 			auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
 
-			if(nodeOptional)
+			if(nodeOptional.has_value())
 			{
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 1
 				logAi->trace("Adding town portal node at %s", targetTown->getObjectName());

+ 3 - 3
AI/Nullkiller2/Pathfinding/AINodeStorage.h

@@ -163,7 +163,7 @@ class AINodeStorage : public INodeStorage
 private:
 	int3 sizes;
 	std::unique_ptr<boost::multi_array<EPathAccessibility, 4>> accessibility;
-	const Nullkiller * aiNk; // TODO: Mircea: Replace with &
+	Nullkiller * aiNk; // TODO: Mircea: Replace with &
 	AISharedStorage nodes;
 	std::vector<std::shared_ptr<ChainActor>> actors;
 	std::vector<CGPathNode *> heroChain;
@@ -176,8 +176,8 @@ private:
 
 public:
 	/// more than 1 chain layer for each hero allows us to have more than 1 path to each tile so we can chose more optimal one.	
-	AINodeStorage(const Nullkiller * aiNk, const int3 & sizes);
-	~AINodeStorage();
+	AINodeStorage(Nullkiller * aiNk, const int3 & sizes);
+	~AINodeStorage() override;
 
 	void initialize(const PathfinderOptions & options, const IGameInfoCallback & gameInfo) override;
 

+ 24 - 19
AI/Nullkiller2/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -215,17 +215,27 @@ namespace AIPathfinding
 	{
 		bool result = false;
 
-		nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
+		nodeStorage->updateAINode(
+			destination.node,
+			[&](const AIPathNode * node)
 			{
-				auto castNodeOptional = nodeStorage->getOrCreateNode(
-					node->coord,
-					node->layer,
-					specialAction->getActor(node->actor));
-
-				if(castNodeOptional)
+				const auto castNodeOptional = nodeStorage->getOrCreateNode(node->coord, node->layer, specialAction->getActor(node->actor));
+				if(!castNodeOptional)
+				{
+#if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
+					logAi->trace(
+						"AILayerTransitionRule::tryUseSpecialAction Failed to allocate node at %s[%d]. "
+						"Can not allocate special transition node while moving %s -> %s",
+						node->coord.toString(),
+						static_cast<int32_t>(node->layer),
+						source.coord.toString(),
+						destination.coord.toString()
+					);
+#endif
+				}
+				else
 				{
 					AIPathNode * castNode = castNodeOptional.value();
-
 					if(castNode->action == EPathNodeAction::UNKNOWN)
 					{
 						castNode->addSpecialAction(specialAction);
@@ -236,22 +246,17 @@ namespace AIPathfinding
 					}
 					else
 					{
-#if NK2AI_PATHFINDER_TRACE_LEVEL >= 1
+#if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
 						logAi->trace(
-							"Special transition node already allocated. Blocked moving %s -> %s",
+							"AILayerTransitionRule::tryUseSpecialAction Special transition node already allocated. Blocked moving %s -> %s",
 							source.coord.toString(),
-							destination.coord.toString());
+							destination.coord.toString()
+						);
 #endif
 					}
 				}
-				else
-				{
-					logAi->debug(
-						"Can not allocate special transition node while moving %s -> %s",
-						source.coord.toString(),
-						destination.coord.toString());
-				}
-			});
+			}
+		);
 
 		return result;
 	}

+ 22 - 8
AI/Nullkiller2/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -180,18 +180,28 @@ namespace AIPathfinding
 		{
 			if(!destinationNode->actor->allowUseResources)
 			{
-				std::optional<AIPathNode *> questNode = nodeStorage->getOrCreateNode(
+				const std::optional<AIPathNode *> questNode = nodeStorage->getOrCreateNode(
 					destination.coord,
 					destination.node->layer,
 					destinationNode->actor->resourceActor);
+				if (!questNode)
+				{
+#if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
+					logAi->trace(
+						"AIMovementAfterDestinationRule::bypassQuest Failed to allocate node at %s[%d]. ",
+						destination.coord.toString(),
+						static_cast<int32_t>(destination.node->layer)
+					);
+#endif
+					return false;
+				}
 
-				if(!questNode || questNode.value()->getCost() < destination.cost)
+				if(questNode.value()->getCost() < destination.cost)
 				{
 					return false;
 				}
 
 				destination.node = questNode.value();
-
 				nodeStorage->commit(destination, source);
 				AIPreviousNodeRule(nodeStorage).process(source, destination, pathfinderConfig, pathfinderHelper);
 			}
@@ -284,18 +294,22 @@ namespace AIPathfinding
 	{
 		const AIPathNode *  srcNode = nodeStorage->getAINode(source.node);
 		const AIPathNode * destNode = nodeStorage->getAINode(destination.node);
-		auto battleNodeOptional = nodeStorage->getOrCreateNode(
+		const auto battleNodeOptional = nodeStorage->getOrCreateNode(
 			destination.coord,
 			destination.node->layer,
 			destNode->actor->battleActor);
 
 		if(!battleNodeOptional)
 		{
-#if NK2AI_PATHFINDER_TRACE_LEVEL >= 1
+#if NK2AI_PATHFINDER_TRACE_LEVEL >= 2
 			logAi->trace(
+				"AIMovementAfterDestinationRule::bypassBattle Failed to allocate node at %s[%d]. "
 				"Can not allocate battle node while moving %s -> %s",
+				destination.coord.toString(),
+				static_cast<int32_t>(destination.node->layer),
 				source.coord.toString(),
-				destination.coord.toString());
+				destination.coord.toString()
+			);
 #endif
 			return false;
 		}
@@ -306,7 +320,7 @@ namespace AIPathfinding
 		{
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 1
 			logAi->trace(
-				"Block bypass guard at destination while moving %s -> %s",
+				"AIMovementAfterDestinationRule::bypassBattle Block bypass guard at destination while moving %s -> %s",
 				source.coord.toString(),
 				destination.coord.toString());
 #endif
@@ -338,7 +352,7 @@ namespace AIPathfinding
 
 #if NK2AI_PATHFINDER_TRACE_LEVEL >= 1
 			logAi->trace(
-				"Begin bypass guard at destination with danger %s while moving %s -> %s",
+				"AIMovementAfterDestinationRule::bypassBattle Begin bypass guard at destination with danger %s while moving %s -> %s",
 				std::to_string(danger),
 				source.coord.toString(),
 				destination.coord.toString());

+ 20 - 20
config/ai/nk2ai/nk2ai-settings.json

@@ -4,8 +4,8 @@
 	"pawn" : {
 		"maxRoamingHeroes" : 3, //H3 value: 3,
 		"maxRoamingHeroesPerTown" : 0,
-		"maxPass" : 30,
-		"maxPriorityPass" : 10,
+		"maxPass" : 7,
+		"maxPriorityPass" : 2,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"threatTurnDistanceLimit" : 1,
@@ -15,8 +15,8 @@
 		"useOneWayMonoliths" : false,
 		"openMap": false,
 		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 3,
-		"pathfinderBucketSize" : 7,
+		"pathfinderBucketsCount" : 1,
+		"pathfinderBucketSize" : 10,
 		"retreatThresholdRelative" : 0,
 		"retreatThresholdAbsolute" : 0,
 		"safeAttackRatio" : 1.1,
@@ -27,8 +27,8 @@
 	"knight" : {
 		"maxRoamingHeroes" : 2, //H3 value: 3,
 		"maxRoamingHeroesPerTown" : 1,
-		"maxPass" : 30,
-		"maxPriorityPass" : 10,
+		"maxPass" : 10,
+		"maxPriorityPass" : 4,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"threatTurnDistanceLimit" : 4,
@@ -38,8 +38,8 @@
 		"useOneWayMonoliths" : false,
 		"openMap": false,
 		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 3,
-		"pathfinderBucketSize" : 14,
+		"pathfinderBucketsCount" : 1,
+		"pathfinderBucketSize" : 20,
 		"retreatThresholdRelative" : 0.1,
 		"retreatThresholdAbsolute" : 5000,
 		"safeAttackRatio" : 1.1,
@@ -50,8 +50,8 @@
 	"rook" : {
 		"maxRoamingHeroes" : 2, //H3 value: 4
 		"maxRoamingHeroesPerTown" : 2,
-		"maxPass" : 30,
-		"maxPriorityPass" : 10,
+		"maxPass" : 40,
+		"maxPriorityPass" : 40,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"threatTurnDistanceLimit" : 5,
@@ -61,8 +61,8 @@
 		"useOneWayMonoliths" : false,
 		"openMap": false,
 		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 3,
-		"pathfinderBucketSize" : 21,
+		"pathfinderBucketsCount" : 1,
+		"pathfinderBucketSize" : 40,
 		"retreatThresholdRelative" : 0.3,
 		"retreatThresholdAbsolute" : 10000,
 		"safeAttackRatio" : 1.1,
@@ -73,8 +73,8 @@
 	"queen" : {
 		"maxRoamingHeroes" : 3, //H3 value: 5
 		"maxRoamingHeroesPerTown" : 2,
-		"maxPass" : 30,
-		"maxPriorityPass" : 10,
+		"maxPass" : 40,
+		"maxPriorityPass" : 40,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"threatTurnDistanceLimit" : 5,
@@ -84,8 +84,8 @@
 		"useOneWayMonoliths" : false,
 		"openMap": true,
 		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 3,
-		"pathfinderBucketSize" : 21,
+		"pathfinderBucketsCount" : 1,
+		"pathfinderBucketSize" : 40,
 		"retreatThresholdRelative" : 0.3,
 		"retreatThresholdAbsolute" : 10000,
 		"safeAttackRatio" : 1.1,
@@ -96,8 +96,8 @@
 	"king" : {
 		"maxRoamingHeroes" : 4, //H3 value: 6
 		"maxRoamingHeroesPerTown" : 2,
-		"maxPass" : 30,
-		"maxPriorityPass" : 10,
+		"maxPass" : 40,
+		"maxPriorityPass" : 40,
 		"mainHeroTurnDistanceLimit" : 10,
 		"scoutHeroTurnDistanceLimit" : 5,
 		"threatTurnDistanceLimit" : 5,
@@ -107,8 +107,8 @@
 		"useOneWayMonoliths" : false,
 		"openMap": true,
 		"allowObjectGraph": false,
-		"pathfinderBucketsCount" : 3,
-		"pathfinderBucketSize" : 21,
+		"pathfinderBucketsCount" : 1,
+		"pathfinderBucketSize" : 40,
 		"retreatThresholdRelative" : 0.3,
 		"retreatThresholdAbsolute" : 10000,
 		"safeAttackRatio" : 1.1,