Browse Source

NKAI: speedup exploration a bit

Andrii Danylchenko 1 year ago
parent
commit
02ea497951

+ 5 - 229
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp

@@ -15,233 +15,15 @@
 #include "../Goals/Composition.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Markers/ExplorationPoint.h"
-#include "CaptureObjectsBehavior.h"
 #include "../Goals/CaptureObject.h"
-#include "../../../lib/CPlayerState.h"
+#include "../Goals/ExploreNeighbourTile.h"
+#include "../Helpers/ExplorationHelper.h"
 
 namespace NKAI
 {
 
 using namespace Goals;
 
-struct ExplorationHelper
-{
-	const CGHeroInstance * hero;
-	int sightRadius;
-	float bestValue;
-	TSubgoal bestGoal;
-	int3 bestTile;
-	int bestTilesDiscovered;
-	const Nullkiller * ai;
-	CCallback * cbp;
-	const TeamState * ts;
-	int3 ourPos;
-	bool allowDeadEndCancellation;
-
-	ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai)
-		:ai(ai), cbp(ai->cb.get()), hero(hero)
-	{
-		ts = cbp->getPlayerTeam(ai->playerID);
-		sightRadius = hero->getSightRadius();
-		bestGoal = sptr(Goals::Invalid());
-		bestValue = 0;
-		bestTilesDiscovered = 0;
-		ourPos = hero->visitablePos();
-		allowDeadEndCancellation = true;
-	}
-
-	TSubgoal makeComposition() const
-	{
-		Composition c;
-		c.addNext(ExplorationPoint(bestTile, bestTilesDiscovered));
-		c.addNext(bestGoal);
-		return sptr(c);
-	}
-
-	void scanSector(int scanRadius)
-	{
-		int3 tile = int3(0, 0, ourPos.z);
-
-		const auto & slice = (*(ts->fogOfWarMap))[ourPos.z];
-
-		for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++)
-		{
-			for(tile.y = ourPos.y - scanRadius; tile.y <= ourPos.y + scanRadius; tile.y++)
-			{
-
-				if(cbp->isInTheMap(tile) && slice[tile.x][tile.y])
-				{
-					scanTile(tile);
-				}
-			}
-		}
-	}
-
-	void 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);
-
-		foreach_tile_pos([&](const int3 & pos)
-			{
-				if((*(ts->fogOfWarMap))[pos.z][pos.x][pos.y])
-				{
-					bool hasInvisibleNeighbor = false;
-
-					foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour)
-						{
-							if(!(*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
-							{
-								hasInvisibleNeighbor = true;
-							}
-						});
-
-					if(hasInvisibleNeighbor)
-						from.push_back(pos);
-				}
-			});
-
-		logAi->debug("Exploration scan visible area perimeter for hero %s", hero->getNameTranslated());
-
-		for(const int3 & tile : from)
-		{
-			scanTile(tile);
-		}
-
-		if(!bestGoal->invalid())
-		{
-			return;
-		}
-
-		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)
-		{
-			scanTile(tile);
-		}
-	}
-
-	void scanTile(const int3 & tile)
-	{
-		if(tile == ourPos
-			|| !ai->pathfinder->isTileAccessible(hero, tile)) //shouldn't happen, but it does
-			return;
-
-		int tilesDiscovered = howManyTilesWillBeDiscovered(tile);
-		if(!tilesDiscovered)
-			return;
-
-		auto paths = ai->pathfinder->getPathInfo(tile);
-		auto waysToVisit = CaptureObjectsBehavior::getVisitGoals(paths, ai, ai->cb->getTopObj(tile));
-
-		for(int i = 0; i != paths.size(); i++)
-		{
-			auto & path = paths[i];
-			auto goal = waysToVisit[i];
-
-			if(path.exchangeCount > 1 || path.targetHero != hero || path.movementCost() <= 0.0 || goal->invalid())
-				continue;
-
-			float ourValue = (float)tilesDiscovered * tilesDiscovered / path.movementCost();
-
-			if(ourValue > bestValue) //avoid costly checks of tiles that don't reveal much
-			{
-				auto obj = cb->getTopObj(tile);
-
-				// picking up resources does not yield any exploration at all.
-				// if it blocks the way to some explorable tile AIPathfinder will take care of it
-				if(obj && obj->isBlockedVisitable())
-				{
-					continue;
-				}
-
-				if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
-				{
-					bestGoal = goal;
-					bestValue = ourValue;
-					bestTile = tile;
-					bestTilesDiscovered = tilesDiscovered;
-				}
-			}
-		}
-	}
-
-	void 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 howManyTilesWillBeDiscovered(const int3 & pos) const
-	{
-		int ret = 0;
-		int3 npos = int3(0, 0, pos.z);
-
-		const auto & slice = (*(ts->fogOfWarMap))[pos.z];
-
-		for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++)
-		{
-			for(npos.y = pos.y - sightRadius; npos.y <= pos.y + sightRadius; npos.y++)
-			{
-				if(cbp->isInTheMap(npos)
-					&& pos.dist2d(npos) - 0.5 < sightRadius
-					&& !slice[npos.x][npos.y])
-				{
-					if(allowDeadEndCancellation
-						&& !hasReachableNeighbor(npos))
-					{
-						continue;
-					}
-
-					ret++;
-				}
-			}
-		}
-
-		return ret;
-	}
-
-	bool hasReachableNeighbor(const int3 & pos) const
-	{
-		for(const int3 & dir : int3::getDirs())
-		{
-			int3 tile = pos + dir;
-			if(cbp->isInTheMap(tile))
-			{
-				auto isAccessible = ai->pathfinder->isTileAccessible(hero, tile);
-
-				if(isAccessible)
-					return true;
-			}
-		}
-
-		return false;
-	}
-};
-
 std::string ExplorationBehavior::toString() const
 {
 	return "Explore";
@@ -301,17 +83,13 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
 	{
 		ExplorationHelper scanResult(hero, ai);
 
-		scanResult.scanSector(1);
-
-		if(!scanResult.bestGoal->invalid())
+		if(scanResult.scanSector(1))
 		{
 			tasks.push_back(scanResult.makeComposition());
 			continue;
 		}
 
-		scanResult.scanSector(15);
-
-		if(!scanResult.bestGoal->invalid())
+		if(scanResult.scanSector(15))
 		{
 			tasks.push_back(scanResult.makeComposition());
 			continue;
@@ -319,9 +97,7 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
 
 		if(ai->getScanDepth() == ScanDepth::ALL_FULL)
 		{
-			scanResult.scanMap();
-
-			if(!scanResult.bestGoal->invalid())
+			if(scanResult.scanMap())
 			{
 				tasks.push_back(scanResult.makeComposition());
 			}

+ 4 - 0
AI/Nullkiller/CMakeLists.txt

@@ -40,6 +40,7 @@ set(Nullkiller_SRCS
 		Goals/ExchangeSwapTownHeroes.cpp
 		Goals/CompleteQuest.cpp
 		Goals/StayAtTown.cpp
+		Goals/ExploreNeighbourTile.cpp
 		Markers/ArmyUpgrade.cpp
 		Markers/HeroExchange.cpp
 		Markers/UnlockCluster.cpp
@@ -62,6 +63,7 @@ set(Nullkiller_SRCS
 		Behaviors/StayAtTownBehavior.cpp
 		Behaviors/ExplorationBehavior.cpp
 		Helpers/ArmyFormation.cpp
+		Helpers/ExplorationHelper.cpp
 		AIGateway.cpp
 )
 
@@ -113,6 +115,7 @@ set(Nullkiller_HEADERS
 		Goals/CompleteQuest.h
 		Goals/Goals.h
 		Goals/StayAtTown.h
+		Goals/ExploreNeighbourTile.h
 		Markers/ArmyUpgrade.h
 		Markers/HeroExchange.h
 		Markers/UnlockCluster.h
@@ -135,6 +138,7 @@ set(Nullkiller_HEADERS
 		Behaviors/StayAtTownBehavior.h
 		Behaviors/ExplorationBehavior.h
 		Helpers/ArmyFormation.h
+		Helpers/ExplorationHelper.h
 		AIGateway.h
 )
 

+ 2 - 1
AI/Nullkiller/Goals/AbstractGoal.h

@@ -75,7 +75,8 @@ namespace Goals
 		STAY_AT_TOWN_BEHAVIOR,
 		STAY_AT_TOWN,
 		EXPLORATION_BEHAVIOR,
-		EXPLORATION_POINT
+		EXPLORATION_POINT,
+		EXPLORE_NEIGHBOUR_TILE
 	};
 
 	class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>

+ 69 - 0
AI/Nullkiller/Goals/ExploreNeighbourTile.cpp

@@ -0,0 +1,69 @@
+/*
+* ExploreNeighbourTile.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 "ExploreNeighbourTile.h"
+#include "../AIGateway.h"
+#include "../AIUtility.h"
+#include "../Helpers/ExplorationHelper.h"
+
+
+namespace NKAI
+{
+
+using namespace Goals;
+
+bool ExploreNeighbourTile::operator==(const ExploreNeighbourTile & other) const
+{
+	return false;
+}
+
+void ExploreNeighbourTile::accept(AIGateway * ai)
+{
+	ExplorationHelper h(hero, ai->nullkiller.get());
+
+	for(int i = 0; i < tilesToExplore && hero->movementPointsRemaining() > 0; i++)
+	{
+		int3 pos = hero->visitablePos();
+		float value = 0;
+		int3 target = int3(-1);
+		foreach_neighbour(pos, [&](int3 tile)
+			{
+				auto pathInfo = ai->myCb->getPathsInfo(hero)->getPathInfo(tile);
+
+				if(pathInfo->turns > 0)
+					return;
+
+				if(pathInfo->accessible == EPathAccessibility::ACCESSIBLE)
+				{
+					float newValue = h.howManyTilesWillBeDiscovered(tile);
+
+					newValue /= std::min(0.1f, pathInfo->getCost());
+
+					if(newValue > value)
+					{
+						value = newValue;
+						target = tile;
+					}
+				}
+			});
+
+		if(!target.valid() || !ai->moveHeroToTile(target, hero))
+		{
+			return;
+		}
+	}
+}
+
+std::string ExploreNeighbourTile::toString() const
+{
+	return "Explore neighbour tiles by " + hero->getNameTranslated();
+}
+
+}

+ 45 - 0
AI/Nullkiller/Goals/ExploreNeighbourTile.h

@@ -0,0 +1,45 @@
+/*
+* ExploreNeighbourTile.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 "CGoal.h"
+
+namespace NKAI
+{
+
+class AIGateway;
+class FuzzyHelper;
+
+namespace Goals
+{
+	class DLL_EXPORT ExploreNeighbourTile : public ElementarGoal<ExploreNeighbourTile>
+	{
+	private:
+		int tilesToExplore;
+
+	public:
+		ExploreNeighbourTile(const CGHeroInstance * hero,  int amount)
+			: ElementarGoal(Goals::EXPLORE_NEIGHBOUR_TILE)
+		{
+			tilesToExplore = amount;
+			sethero(hero);
+		}
+
+		bool operator==(const ExploreNeighbourTile & other) const override;
+
+		void accept(AIGateway * ai) override;
+		std::string toString() const override;
+
+	private:
+		//TSubgoal decomposeSingle() const override;
+	};
+}
+
+}

+ 235 - 0
AI/Nullkiller/Helpers/ExplorationHelper.cpp

@@ -0,0 +1,235 @@
+/*
+* ExplorationHelper.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 "ExplorationHelper.h"
+#include "../../../lib/mapObjects/CGTownInstance.h"
+#include "../Engine/Nullkiller.h"
+#include "../Goals/Invalid.h"
+#include "../Goals/Composition.h"
+#include "../Goals/ExecuteHeroChain.h"
+#include "../Markers/ExplorationPoint.h"
+#include "../../../lib/CPlayerState.h"
+#include "../Behaviors/CaptureObjectsBehavior.h"
+#include "../Goals/ExploreNeighbourTile.h"
+
+namespace NKAI
+{
+
+using namespace Goals;
+
+ExplorationHelper::ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai)
+	:ai(ai), cbp(ai->cb.get()), hero(hero)
+{
+	ts = cbp->getPlayerTeam(ai->playerID);
+	sightRadius = hero->getSightRadius();
+	bestGoal = sptr(Goals::Invalid());
+	bestValue = 0;
+	bestTilesDiscovered = 0;
+	ourPos = hero->visitablePos();
+	allowDeadEndCancellation = true;
+}
+
+TSubgoal ExplorationHelper::makeComposition() const
+{
+	Composition c;
+	c.addNext(ExplorationPoint(bestTile, bestTilesDiscovered));
+	c.addNextSequence({bestGoal, sptr(ExploreNeighbourTile(hero, 5))});
+	return sptr(c);
+}
+
+
+bool ExplorationHelper::scanSector(int scanRadius)
+{
+	int3 tile = int3(0, 0, ourPos.z);
+
+	const auto & slice = (*(ts->fogOfWarMap))[ourPos.z];
+
+	for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++)
+	{
+		for(tile.y = ourPos.y - scanRadius; tile.y <= ourPos.y + scanRadius; tile.y++)
+		{
+			if(cbp->isInTheMap(tile) && slice[tile.x][tile.y])
+			{
+				scanTile(tile);
+			}
+		}
+	}
+
+	return !bestGoal->invalid();
+}
+
+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);
+
+	foreach_tile_pos([&](const int3 & pos)
+		{
+			if((*(ts->fogOfWarMap))[pos.z][pos.x][pos.y])
+			{
+				bool hasInvisibleNeighbor = false;
+
+				foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour)
+					{
+						if(!(*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
+						{
+							hasInvisibleNeighbor = true;
+						}
+					});
+
+				if(hasInvisibleNeighbor)
+					from.push_back(pos);
+			}
+		});
+
+	logAi->debug("Exploration scan visible area perimeter for hero %s", hero->getNameTranslated());
+
+	for(const int3 & tile : from)
+	{
+		scanTile(tile);
+	}
+
+	if(!bestGoal->invalid())
+	{
+		return false;
+	}
+
+	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)
+	{
+		scanTile(tile);
+	}
+
+	return !bestGoal->invalid();
+}
+
+void ExplorationHelper::scanTile(const int3 & tile)
+{
+	if(tile == ourPos
+		|| !ai->cb->getTile(tile, false)
+		|| !ai->pathfinder->isTileAccessible(hero, tile)) //shouldn't happen, but it does
+		return;
+
+	int tilesDiscovered = howManyTilesWillBeDiscovered(tile);
+	if(!tilesDiscovered)
+		return;
+	
+	auto paths = ai->pathfinder->getPathInfo(tile);
+	auto waysToVisit = CaptureObjectsBehavior::getVisitGoals(paths, ai, ai->cb->getTopObj(tile));
+
+	for(int i = 0; i != paths.size(); i++)
+	{
+		auto & path = paths[i];
+		auto goal = waysToVisit[i];
+
+		if(path.exchangeCount > 1 || path.targetHero != hero || path.movementCost() <= 0.0 || goal->invalid())
+			continue;
+
+		float ourValue = (float)tilesDiscovered * tilesDiscovered / path.movementCost();
+
+		if(ourValue > bestValue) //avoid costly checks of tiles that don't reveal much
+		{
+			auto obj = cb->getTopObj(tile);
+
+			// picking up resources does not yield any exploration at all.
+			// if it blocks the way to some explorable tile AIPathfinder will take care of it
+			if(obj && obj->isBlockedVisitable())
+			{
+				continue;
+			}
+
+			if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
+			{
+				bestGoal = goal;
+				bestValue = ourValue;
+				bestTile = tile;
+				bestTilesDiscovered = tilesDiscovered;
+			}
+		}
+	}
+}
+
+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;
+	int3 npos = int3(0, 0, pos.z);
+
+	const auto & slice = (*(ts->fogOfWarMap))[pos.z];
+
+	for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++)
+	{
+		for(npos.y = pos.y - sightRadius; npos.y <= pos.y + sightRadius; npos.y++)
+		{
+			if(cbp->isInTheMap(npos)
+				&& pos.dist2d(npos) - 0.5 < sightRadius
+				&& !slice[npos.x][npos.y])
+			{
+				if(allowDeadEndCancellation
+					&& !hasReachableNeighbor(npos))
+				{
+					continue;
+				}
+
+				ret++;
+			}
+		}
+	}
+
+	return ret;
+}
+
+bool ExplorationHelper::hasReachableNeighbor(const int3 & pos) const
+{
+	for(const int3 & dir : int3::getDirs())
+	{
+		int3 tile = pos + dir;
+		if(cbp->isInTheMap(tile))
+		{
+			auto isAccessible = ai->pathfinder->isTileAccessible(hero, tile);
+
+			if(isAccessible)
+				return true;
+		}
+	}
+
+	return false;
+}
+
+}

+ 51 - 0
AI/Nullkiller/Helpers/ExplorationHelper.h

@@ -0,0 +1,51 @@
+/*
+* ExplorationHelper.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 "../AIUtility.h"
+
+#include "../../../lib/GameConstants.h"
+#include "../../../lib/VCMI_Lib.h"
+#include "../../../lib/CTownHandler.h"
+#include "../../../lib/CBuildingHandler.h"
+#include "../Goals/AbstractGoal.h"
+
+namespace NKAI
+{
+
+class ExplorationHelper
+{
+private:
+	const CGHeroInstance * hero;
+	int sightRadius;
+	float bestValue;
+	Goals::TSubgoal bestGoal;
+	int3 bestTile;
+	int bestTilesDiscovered;
+	const Nullkiller * ai;
+	CCallback * cbp;
+	const TeamState * ts;
+	int3 ourPos;
+	bool allowDeadEndCancellation;
+
+public:
+	ExplorationHelper(const CGHeroInstance * hero, const Nullkiller * ai);
+	Goals::TSubgoal makeComposition() const;
+	bool scanSector(int scanRadius);
+	bool scanMap();
+	int howManyTilesWillBeDiscovered(const int3 & pos) const;
+
+private:
+	void scanTile(const int3 & tile);
+	bool hasReachableNeighbor(const int3 & pos) const;
+	void getVisibleNeighbours(const std::vector<int3> & tiles, std::vector<int3> & out) const;
+};
+
+}