Forráskód Böngészése

Endless crusade against AI issues and loopholes!
- Fixed #1126
- ClearWayTo and GatherArmy goals will also consider multiple subgoals
- GatherArmy may include building dwellings in town (experimental)

DjWarmonger 12 éve
szülő
commit
aec04d920e
7 módosított fájl, 321 hozzáadás és 203 törlés
  1. 2 2
      AI/VCAI/AIUtility.cpp
  2. 11 2
      AI/VCAI/Fuzzy.cpp
  3. 1 0
      AI/VCAI/Fuzzy.h
  4. 136 129
      AI/VCAI/Goals.cpp
  5. 2 2
      AI/VCAI/Goals.h
  6. 150 65
      AI/VCAI/VCAI.cpp
  7. 19 3
      AI/VCAI/VCAI.h

+ 2 - 2
AI/VCAI/AIUtility.cpp

@@ -237,6 +237,7 @@ int3 whereToExplore(HeroPtr h)
 			}
 		}
 	}
+	removeDuplicates (nearbyVisitableObjs); //one object may occupy multiple tiles
 	boost::sort(nearbyVisitableObjs, isCloser);
 	if(nearbyVisitableObjs.size())
 		return nearbyVisitableObjs.back()->visitablePos();
@@ -247,8 +248,7 @@ int3 whereToExplore(HeroPtr h)
 	}
 	catch(cannotFulfillGoalException &e)
 	{
-		std::vector<std::vector<int3> > tiles; //tiles[distance_to_fow]
-		return ai->explorationNewPoint(radius, h, tiles);
+		return ai->explorationNewPoint(radius, h);
 	}
 }
 

+ 11 - 2
AI/VCAI/Fuzzy.cpp

@@ -337,6 +337,7 @@ FuzzyHelper::EvalVisitTile::~EvalVisitTile()
 	delete heroStrength;
 	delete tileDistance;
 	delete missionImportance;
+	delete movement;
 }
 
 void FuzzyHelper::initVisitTile()
@@ -347,9 +348,10 @@ void FuzzyHelper::initVisitTile()
 	vt.heroStrength = new fl::InputLVar("heroStrength"); //we want to use weakest possible hero
 	vt.tileDistance = new fl::InputLVar("tileDistance"); //we want to use hero who is near
 	vt.missionImportance = new fl::InputLVar("lockedMissionImportance"); //we may want to preempt hero with low-priority mission
+	vt.movement = new fl::InputLVar("movement");
 	vt.value = new fl::OutputLVar("Value");
 
-	helper += vt.strengthRatio, vt.heroStrength, vt.tileDistance, vt.missionImportance;
+	helper += vt.strengthRatio, vt.heroStrength, vt.tileDistance, vt.missionImportance, vt.movement;
 
 	vt.strengthRatio->addTerm (new fl::ShoulderTerm("LOW", 0.9, SAFE_ATTACK_CONSTANT, true));
 	vt.strengthRatio->addTerm (new fl::ShoulderTerm("HIGH", SAFE_ATTACK_CONSTANT, SAFE_ATTACK_CONSTANT * 3, false));
@@ -368,6 +370,9 @@ void FuzzyHelper::initVisitTile()
 
 	vt.value->addTerm (new fl::ShoulderTerm("LOW", 0, 1.1, true));
 	vt.value->addTerm (new fl::ShoulderTerm("HIGH", 1, 5, false));
+
+	vt.movement->addTerm (new fl::ShoulderTerm("LOW", 1, 200, true));
+	vt.movement->addTerm (new fl::ShoulderTerm("HIGH", 1000, 2000, false));
 	
 	for (auto val : helper)
 	{
@@ -387,6 +392,9 @@ void FuzzyHelper::initVisitTile()
 	//pick nearby objects if it's easy, avoid long walks
 	vt.rules.addRule (new fl::MamdaniRule("if tileDistance is SMALL then Value is HIGH", engine));
 	vt.rules.addRule (new fl::MamdaniRule("if tileDistance is LONG then Value is LOW", engine));
+	//use heroes with movement points first
+	vt.rules.addRule (new fl::MamdaniRule("if movement is LOW then Value is somewhat LOW", engine));
+	vt.rules.addRule (new fl::MamdaniRule("if movement is HIGH then Value is somewhat HIGH", engine));
 
 	engine.addRuleBlock (&vt.rules);
 }
@@ -416,6 +424,7 @@ float FuzzyHelper::evaluate (Goals::VisitTile & g)
 		vt.heroStrength->setInput (g.hero->getTotalStrength());
 		vt.tileDistance->setInput (distance);
 		vt.missionImportance->setInput (missionImportance);
+		vt.movement->setInput(g.hero->movement);
 
 		engine.process (VISIT_TILE);
 		output = vt.value->output().defuzzify();
@@ -435,7 +444,7 @@ float FuzzyHelper::evaluate (Goals::VisitHero & g)
 }
 float FuzzyHelper::evaluate (Goals::BuildThis & g)
 {
-	return 0;
+	return 1;
 }
 float FuzzyHelper::evaluate (Goals::DigAtTile & g)
 {

+ 1 - 0
AI/VCAI/Fuzzy.h

@@ -47,6 +47,7 @@ class FuzzyHelper
 		fl::InputLVar * heroStrength;
 		fl::InputLVar * tileDistance;
 		fl::InputLVar * missionImportance;
+		fl::InputLVar * movement;
 		fl::OutputLVar * value;
 		fl::RuleBlock rules;
 		~EvalVisitTile();

+ 136 - 129
AI/VCAI/Goals.cpp

@@ -29,6 +29,7 @@ TSubgoal Goals::sptr(const AbstractGoal & tmp)
 
 std::string Goals::AbstractGoal::name() const //TODO: virtualize
 {
+	std::string desc;
 	switch (goalType)
 	{
 		case INVALID:
@@ -42,38 +43,53 @@ std::string Goals::AbstractGoal::name() const //TODO: virtualize
 		case BUILD:
 			return "BUILD";
 		case EXPLORE:
-			return "EXPLORE";
+			desc = "EXPLORE";
+			break;
 		case GATHER_ARMY:
-			return "GATHER ARMY";
+			desc = "GATHER ARMY";
+			break;
 		case BOOST_HERO:
-			return "BOOST_HERO (unsupported)";
+			desc = "BOOST_HERO (unsupported)";
+			break;
 		case RECRUIT_HERO:
 			return "RECRUIT HERO";
 		case BUILD_STRUCTURE:
 			return "BUILD STRUCTURE";
 		case COLLECT_RES:
-			return "COLLECT RESOURCE";
+			desc = "COLLECT RESOURCE";
+			break;
 		case GATHER_TROOPS:
-			return "GATHER TROOPS";
+			desc = "GATHER TROOPS";
+			break;
 		case GET_OBJ:
-			return "GET OBJECT " + boost::lexical_cast<std::string>(objid);
+			desc = "GET OBJ " + cb->getObjInstance(ObjectInstanceID(objid))->getHoverText();
+			break;
 		case FIND_OBJ:
-			return "FIND OBJECT " + boost::lexical_cast<std::string>(objid);
+			desc = "FIND OBJ " + boost::lexical_cast<std::string>(objid);
+			break;
 		case VISIT_HERO:
-			return "VISIT HERO " + boost::lexical_cast<std::string>(objid);
+			desc = "VISIT HERO " + cb->getObjInstance(ObjectInstanceID(objid))->getHoverText();
+			break;
 		case GET_ART_TYPE:
-			return "GET ARTIFACT OF TYPE " + VLC->arth->artifacts[aid]->Name();
+			desc = "GET ARTIFACT OF TYPE " + VLC->arth->artifacts[aid]->Name();
+			break;
 		case ISSUE_COMMAND:
 			return "ISSUE COMMAND (unsupported)";
 		case VISIT_TILE:
-			return "VISIT TILE " + tile();
+			desc = "VISIT TILE " + tile();
+			break;
 		case CLEAR_WAY_TO:
-			return "CLEAR WAY TO " + tile();
+			desc = "CLEAR WAY TO " + tile();
+			break;
 		case DIG_AT_TILE:
-			return "DIG AT TILE " + tile();
+			desc = "DIG AT TILE " + tile();
+			break;
 		default:
 			return boost::lexical_cast<std::string>(goalType);
 	}
+	if (hero.h)
+		desc += " (" + hero->name + ")";
+	return desc;
 }
 
 //TODO: find out why the following are not generated automatically on MVS?
@@ -312,52 +328,56 @@ float GetArtOfType::importanceWhenLocked() const
 
 TSubgoal ClearWayTo::whatToDoToAchieve()
 {
-	assert(tile.x >= 0); //set tile
+	assert(cb->isInTheMap(tile)); //set tile
 	if(!cb->isVisible(tile))
 	{
         logAi->errorStream() << "Clear way should be used with visible tiles!";
 		return sptr (Goals::Explore());
 	}
 
-	HeroPtr h = hero ? hero : ai->primaryHero();
-	if(!h)
-		return sptr (Goals::RecruitHero());
+	return (fh->chooseSolution(getAllPossibleSubgoals()));	
+}
 
-	cb->setSelection(*h);
+TGoalVec ClearWayTo::getAllPossibleSubgoals()
+{
+	TGoalVec ret;
+	for (auto h : cb->getHeroesInfo())
+	{
+		cb->setSelection(h);
 
-	SectorMap sm;
-	bool dropToFile = false;
-	if(dropToFile) //for debug purposes
-		sm.write("test.txt");
+		SectorMap sm;
 
-	int3 tileToHit = sm.firstTileToGet(h, tile);
-	//if(isSafeToVisit(h, tileToHit))
-	if(isBlockedBorderGate(tileToHit))
-	{	//FIXME: this way we'll not visit gate and activate quest :?
-		return sptr (Goals::FindObj (Obj::KEYMASTER, cb->getTile(tileToHit)->visitableObjects.back()->subID));
-	}
+		int3 tileToHit = sm.firstTileToGet(hero ? hero : h, tile);
+		//if our hero is trapped, make sure we request clearing the way from OUR perspective
 
-	//FIXME: this code shouldn't be necessary
-	if(tileToHit == tile)
-	{
-        logAi->errorStream() << boost::format("Very strange, tile to hit is %s and tile is also %s, while hero %s is at %s\n")
-			% tileToHit % tile % h->name % h->visitablePos();
-		throw cannotFulfillGoalException("Retrieving first tile to hit failed (probably)!");
-	}
+		if (isBlockedBorderGate(tileToHit))
+		{	//FIXME: this way we'll not visit gate and activate quest :?
+			ret.push_back  (sptr (Goals::FindObj (Obj::KEYMASTER, cb->getTile(tileToHit)->visitableObjects.back()->subID)));
+		}
 
-	auto topObj = backOrNull(cb->getVisitableObjs(tileToHit));
-	if(topObj && topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES)
-	{
-		std::string problem = boost::str(boost::format("%s stands in the way of %s.\n") % topObj->getHoverText()  % h->getHoverText());
-		throw cannotFulfillGoalException(problem);
-	}
+		////FIXME: this code shouldn't be necessary
+		//if(tileToHit == tile)
+		//{
+		//	logAi->errorStream() << boost::format("Very strange, tile to hit is %s and tile is also %s, while hero %s is at %s\n")
+		//		% tileToHit % tile % h->name % h->visitablePos();
+		//	throw cannotFulfillGoalException("Retrieving first tile to hit failed (probably)!");
+		//}
 
-	return sptr (Goals::VisitTile(tileToHit).sethero(h));
-	//FIXME:: attempts to visit completely unreachable tile with hero results in stall
+		auto topObj = backOrNull(cb->getVisitableObjs(tileToHit));
+		if(topObj && topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES)
+		{
+			logAi->errorStream() << boost::format("%s stands in the way of %s.\n") % topObj->getHoverText()  % h->getHoverText();
+		}
+		else
+			ret.push_back (sptr (Goals::VisitTile(tileToHit).sethero(h)));
+	}
+	if (ai->canRecruitAnyHero())
+		ret.push_back (sptr (Goals::RecruitHero()));
 
-	//TODO czy istnieje lepsza droga?
+	if (ret.empty())
+		throw cannotFulfillGoalException("There is no known way to clear the way to tile " + tile());
 
-	throw cannotFulfillGoalException("Cannot reach given tile!"); //how and when could this be used?
+	return ret;
 }
 
 float ClearWayTo::importanceWhenLocked() const
@@ -387,12 +407,15 @@ TSubgoal Explore::whatToDoToAchieve()
 TGoalVec Explore::getAllPossibleSubgoals()
 {
 	TGoalVec ret;
-	std::vector<HeroPtr> heroes;
+	std::vector<const CGHeroInstance *> heroes;
+	//std::vector<HeroPtr> heroes;
 	if (hero)
-		heroes.push_back(hero);
+		//heroes.push_back(hero);
+		heroes.push_back(hero.h);
 	else
 	{
-		heroes = ai->getUnblockedHeroes();
+		//heroes = ai->getUnblockedHeroes();
+		heroes = cb->getHeroesInfo();
 		erase_if (heroes, [](const HeroPtr h)
 		{
 			return !h->movement; //saves time, immobile heroes are useless anyway
@@ -429,9 +452,20 @@ TGoalVec Explore::getAllPossibleSubgoals()
 		if (cb->isInTheMap(t)) //valid tile was found - could be invalid (none)
 			ret.push_back (sptr (Goals::VisitTile(t).sethero(h)));
 	}
-	if (!hero && ai->canRecruitAnyHero())//if hero is assigned to that goal, no need to buy another one yet
+	//we either don't have hero yet or none of heroes can explore
+	if ((!hero || ret.empty()) && ai->canRecruitAnyHero())
 		ret.push_back (sptr(Goals::RecruitHero()));
 
+	if (ret.empty())
+	{
+		auto h = ai->primaryHero(); //we may need to gather big army to break!
+		if (h.h)
+		{
+			int3 t = ai->explorationNewPoint(h->getSightRadious(), h, true);
+			if (cb->isInTheMap(t))
+				ret.push_back (sptr(ClearWayTo(t).setisAbstract(true).sethero(ai->primaryHero())));
+		}
+	}
 	if (ret.empty())
 		throw cannotFulfillGoalException("Cannot explore - no possible ways found!");
 
@@ -473,7 +507,8 @@ TSubgoal VisitTile::whatToDoToAchieve()
 		}
 		else
 		{
-			return sptr (Goals::GatherArmy(evaluateDanger(tile, *ret->hero) * SAFE_ATTACK_CONSTANT).sethero(ret->hero));
+			return sptr (Goals::GatherArmy(evaluateDanger(tile, *ret->hero) * SAFE_ATTACK_CONSTANT)
+						.sethero(ret->hero).setisAbstract(true));
 		}
 	}
 	return ret;
@@ -507,6 +542,7 @@ TGoalVec VisitTile::getAllPossibleSubgoals()
 	}
 	if (ret.empty())
 		ret.push_back (sptr(Goals::ClearWayTo(tile)));
+
 	//important - at least one sub-goal must handle case which is impossible to fulfill (unreachable tile)
 	return ret;
 }
@@ -746,68 +782,54 @@ std::string GatherArmy::completeMessage() const
 TSubgoal GatherArmy::whatToDoToAchieve()
 {
 	//TODO: find hero if none set
-	assert(hero);
+	assert(hero.h);
 
-	cb->setSelection(*hero);
-	auto compareReinforcements = [this](const CGTownInstance *lhs, const CGTownInstance *rhs) -> bool
-	{
-		return howManyReinforcementsCanGet(hero, lhs) < howManyReinforcementsCanGet(hero, rhs);
-	};
-
-	std::vector<const CGTownInstance *> townsReachable;
-	for(const CGTownInstance *t : cb->getTownsInfo())
+	return fh->chooseSolution (getAllPossibleSubgoals()); //find dwelling. use current hero to prevent him from doing nothing.
+}
+TGoalVec GatherArmy::getAllPossibleSubgoals()
+{
+	//get all possible towns, heroes and dwellings we may use
+	TGoalVec ret;
+	
+	//TODO: include evaluation of monsters gather in calculation
+	for (auto t : cb->getTownsInfo())
 	{
-		if(!t->visitingHero && howManyReinforcementsCanGet(hero,t))
+		auto pos = t->visitablePos();
+		if (ai->isAccessibleForHero(pos, hero))
 		{
-			if (ai->isAccessibleForHero(t->pos, hero) && !vstd::contains (ai->townVisitsThisWeek[hero], t))
-				townsReachable.push_back(t);
+			if(!t->visitingHero && howManyReinforcementsCanGet(hero,t))
+			{
+				if (!vstd::contains (ai->townVisitsThisWeek[hero], t))
+					ret.push_back (sptr (Goals::VisitTile(pos).sethero(hero)));
+			}
+			auto bid = ai->canBuildAnyStructure(t, std::vector<BuildingID>
+							(unitsSource, unitsSource + ARRAY_COUNT(unitsSource)), 8 - cb->getDate(Date::DAY_OF_WEEK));
+			if (bid != BuildingID::NONE)
+				ret.push_back (sptr(BuildThis(bid, t)));
 		}
 	}
 
-	if(townsReachable.size()) //try towns first
+	auto otherHeroes = cb->getHeroesInfo();
+	auto heroDummy = hero;
+	erase_if(otherHeroes, [heroDummy](const CGHeroInstance * h)
 	{
-		boost::sort(townsReachable, compareReinforcements);
-		return sptr (Goals::VisitTile(townsReachable.back()->visitablePos()).sethero(hero));
-	}
-	else
+		return (h == heroDummy.h || !ai->isAccessibleForHero(heroDummy->visitablePos(), h, true) || !ai->canGetArmy(heroDummy.h, h));
+	});
+	for (auto h : otherHeroes)
 	{
-		if (hero == ai->primaryHero()) //we can get army from other heroes
-		{
-			auto otherHeroes = cb->getHeroesInfo();
-			auto heroDummy = hero;
-			erase_if(otherHeroes, [heroDummy](const CGHeroInstance * h)
-			{
-				return (h == heroDummy.h || !ai->isAccessibleForHero(heroDummy->visitablePos(), h, true) || !ai->canGetArmy(heroDummy.h, h));
-			});
-			if (otherHeroes.size())
-			{
-				boost::sort(otherHeroes, compareArmyStrength); //TODO:  check if hero has at least one stack more powerful than ours? not likely to fail
-				int primaryPath, secondaryPath;
-				auto h = otherHeroes.back();
-				cb->setSelection(hero.h);
-				primaryPath = cb->getPathInfo(h->visitablePos())->turns;
-				cb->setSelection(h);
-				secondaryPath = cb->getPathInfo(hero->visitablePos())->turns;
-
-				if (primaryPath < secondaryPath)
-					return sptr (Goals::VisitHero(h->id.getNum()).setisAbstract(true).sethero(hero));
-						//go to the other hero if we are faster
-				else
-					return sptr (Goals::VisitHero(hero->id.getNum()).setisAbstract(true).sethero(h));
-						//let the other hero come to us
-			}
-		}
 
-		std::vector<const CGObjectInstance *> objs; //here we'll gather all dwellings
-		ai->retreiveVisitableObjs(objs, true);
-		erase_if(objs, [&](const CGObjectInstance *obj)
-		{
-			if(obj->ID != Obj::CREATURE_GENERATOR1)
-				return true;
+		ret.push_back (sptr (Goals::VisitHero(h->id.getNum()).setisAbstract(true).sethero(hero)));
+				//go to the other hero if we are faster
+		ret.push_back (sptr (Goals::VisitHero(hero->id.getNum()).setisAbstract(true).sethero(h)));
+				//let the other hero come to us
+	}
 
+	std::vector <const CGObjectInstance *> objs;
+	for (auto obj : ai->visitableObjs)
+	{
+		if(obj->ID == Obj::CREATURE_GENERATOR1)
+		{
 			auto relationToOwner = cb->getPlayerRelations(obj->getOwner(), ai->playerID);
-			if(relationToOwner == PlayerRelations::ALLIES)
-				return true;
 
 			//Use flagged dwellings only when there are available creatures that we can afford
 			if(relationToOwner == PlayerRelations::SAME_PLAYER)
@@ -820,42 +842,27 @@ TSubgoal GatherArmy::whatToDoToAchieve()
 						for(auto & creatureID : creLevel.second)
 						{
 							auto creature = VLC->creh->creatures[creatureID];
-							if(ai->freeResources().canAfford(creature->cost))
-								return false;
+							if (ai->freeResources().canAfford(creature->cost))
+								objs.push_back(obj);
 						}
 					}
 				}
 			}
-
-			return true;
-		});
-		if(objs.empty()) //no possible objects, we did eveyrthing already
-			return sptr (Goals::Explore(hero));
-		//TODO: check if we can recruit any creatures there, evaluate army
-		else
-		{
-			boost::sort(objs, isCloser);
-			HeroPtr h = nullptr;
-			for(const CGObjectInstance *obj : objs)
-			{ //find safe dwelling
-				auto pos = obj->visitablePos();
-				if (shouldVisit (hero, obj)) //creatures fit in army
-					h = hero;
-				else
-				{
-					for(auto ourHero : cb->getHeroesInfo()) //make use of multiple heroes
-					{
-						if (shouldVisit(ourHero, obj))
-							h = ourHero;
-					}
-				}
-				if (h && isSafeToVisit(h, pos) && ai->isAccessibleForHero(pos, h))
-					return sptr (Goals::VisitTile(pos).sethero(h));
-			}
 		}
 	}
+	for(auto h : cb->getHeroesInfo())
+	{
+		for (auto obj : objs)
+		{ //find safe dwelling
+			auto pos = obj->visitablePos();
+			if (shouldVisit (h, obj) && isSafeToVisit(h, pos) && ai->isAccessibleForHero(pos, h))
+				ret.push_back (sptr (Goals::VisitTile(pos).sethero(h)));
+		}
+	}
+	if (ret.empty())
+		ret.push_back (sptr(Goals::Explore()));
 
-	return sptr (Goals::Explore(hero)); //find dwelling. use current hero to prevent him from doing nothing.
+	return ret;
 }
 
 float GatherArmy::importanceWhenLocked() const

+ 2 - 2
AI/VCAI/Goals.h

@@ -240,7 +240,7 @@ private:
 	GatherArmy() : CGoal (Goals::GATHER_ARMY){};
 public:
 	GatherArmy(int val) : CGoal (Goals::GATHER_ARMY){value = val;};
-	TGoalVec getAllPossibleSubgoals() override {return TGoalVec();};
+	TGoalVec getAllPossibleSubgoals() override;
 	TSubgoal whatToDoToAchieve() override;
 	std::string completeMessage() const override;
 	float importanceWhenLocked() const override;
@@ -356,7 +356,7 @@ class ClearWayTo : public CGoal<ClearWayTo>
 {
 public:
 	ClearWayTo(int3 Tile) : CGoal (Goals::CLEAR_WAY_TO) {tile = Tile;};
-	TGoalVec getAllPossibleSubgoals() override {return TGoalVec();};
+	TGoalVec getAllPossibleSubgoals() override;
 	TSubgoal whatToDoToAchieve() override;
 	bool operator== (ClearWayTo &g) {return g.tile == tile;}
 	float importanceWhenLocked() const override;

+ 150 - 65
AI/VCAI/VCAI.cpp

@@ -406,9 +406,11 @@ void VCAI::newStackInserted(const StackLocation &location, const CStackInstance
 	NET_EVENT_HANDLER;
 }
 
-void VCAI::heroCreated(const CGHeroInstance*)
+void VCAI::heroCreated(const CGHeroInstance* h)
 {
 	LOG_TRACE(logAi);
+	if (h->visitedTown)
+		townVisitsThisWeek[HeroPtr(h)].push_back(h->visitedTown);
 	NET_EVENT_HANDLER;
 }
 
@@ -523,6 +525,9 @@ void VCAI::init(shared_ptr<CCallback> CB)
 		fh = new FuzzyHelper();
 
 	retreiveVisitableObjs(visitableObjs);
+	//for (auto h : myCb->getHeroesInfo()) //make sure heroes won't try to revisit town in first move
+	//	if (h->visitedTown)
+	//		markObjectVisited(h->visitedTown);
 }
 
 void VCAI::yourTurn()
@@ -651,30 +656,6 @@ void VCAI::makeTurn()
 			}
 		}
 			break;
-		case 7: //reconsider strategy
-		{
-			if(auto h = primaryHero()) //check if our primary hero can handle danger
-			{
-				ui64 totalDanger = 0;
-				int dangerousObjects = 0;
-				std::vector<const CGObjectInstance *> objs;
-				retreiveVisitableObjs(objs, false);
-				for (auto obj : objs)
-				{
-					if (evaluateDanger(obj)) //potentilaly dnagerous
-					{
-						totalDanger += evaluateDanger(obj->visitablePos(), *h);
-						++dangerousObjects;
-					}
-				}
-				ui64 averageDanger = totalDanger / std::max(dangerousObjects, 1);
-				if (dangerousObjects && averageDanger > h->getHeroStrength())
-				{
-					setGoal (h, sptr(Goals::GatherArmy(averageDanger * SAFE_ATTACK_CONSTANT).sethero(h).setisAbstract(true)));
-				}
-			}
-		}
-			break;
 	}
 	if(cb->getSelectedHero())
 		cb->recalculatePaths();
@@ -1006,6 +987,84 @@ bool VCAI::tryBuildStructure(const CGTownInstance * t, BuildingID building, unsi
 	return false;
 }
 
+//bool VCAI::canBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays=7)
+//{
+//		if (maxDays == 0)
+//	{
+//		logAi->warnStream() << "Request to build building " << building <<  " in 0 days!";
+//		return false;
+//	}
+//
+//	if (!vstd::contains(t->town->buildings, building))
+//		return false; // no such building in town
+//
+//	if (t->hasBuilt(building)) //Already built? Shouldn't happen in general
+//		return true;
+//
+//	const CBuilding * buildPtr = t->town->buildings.at(building);
+//
+//	auto toBuild = buildPtr->requirements.getFulfillmentCandidates([&](const BuildingID & buildID)
+//	{
+//		return t->hasBuilt(buildID);
+//	});
+//	toBuild.push_back(building);
+//
+//	for(BuildingID buildID : toBuild)
+//	{
+//		EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID);
+//		if (canBuild == EBuildingState::HAVE_CAPITAL
+//		 || canBuild == EBuildingState::FORBIDDEN
+//		 || canBuild == EBuildingState::NO_WATER)
+//			return false; //we won't be able to build this
+//	}
+//
+//	if (maxDays && toBuild.size() > maxDays)
+//		return false;
+//
+//	TResources currentRes = cb->getResourceAmount();
+//	TResources income = estimateIncome();
+//	//TODO: calculate if we have enough resources to build it in maxDays
+//
+//	for(const auto & buildID : toBuild)
+//	{
+//		const CBuilding *b = t->town->buildings.at(buildID);
+//
+//		EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID);
+//		if(canBuild == EBuildingState::ALLOWED)
+//		{
+//			if(!containsSavedRes(b->resources))
+//			{
+//                logAi->debugStream() << boost::format("Player %d will build %s in town of %s at %s") % playerID % b->Name() % t->name % t->pos;
+//				return true;
+//			}
+//			continue;
+//		}
+//		else if(canBuild == EBuildingState::NO_RESOURCES)
+//		{
+//			TResources cost = t->town->buildings.at(buildID)->resources;
+//			for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
+//			{
+//				int diff = currentRes[i] - cost[i] + income[i];
+//				if(diff < 0)
+//					saving[i] = 1;
+//			}
+//			continue;
+//		}
+//		else if (canBuild == EBuildingState::PREREQUIRES)
+//		{
+//			// can happen when dependencies have their own missing dependencies
+//			if (canBuildStructure(t, buildID, maxDays - 1))
+//				return true;
+//		}
+//		else if (canBuild == EBuildingState::MISSING_BASE)
+//		{
+//			if (canBuildStructure(t, b->upgrade, maxDays - 1))
+//				 return true;
+//		}
+//	}
+//	return false;
+//}
+
 bool VCAI::tryBuildAnyStructure(const CGTownInstance * t, std::vector<BuildingID> buildList, unsigned int maxDays)
 {
 	for(const auto & building : buildList)
@@ -1018,6 +1077,18 @@ bool VCAI::tryBuildAnyStructure(const CGTownInstance * t, std::vector<BuildingID
 	return false; //Can't build anything
 }
 
+BuildingID VCAI::canBuildAnyStructure(const CGTownInstance * t, std::vector<BuildingID> buildList, unsigned int maxDays)
+{
+	for(const auto & building : buildList)
+	{
+		if(t->hasBuilt(building))
+			continue;
+		if (cb->canBuildStructure(t, building))
+			return building;
+	}
+	return BuildingID::NONE; //Can't build anything
+}
+
 bool VCAI::tryBuildNextStructure(const CGTownInstance * t, std::vector<BuildingID> buildList, unsigned int maxDays)
 {
 	for(const auto & building : buildList)
@@ -1036,20 +1107,6 @@ void VCAI::buildStructure(const CGTownInstance * t)
 	//TODO: build resource silo, defences when needed
 	//Possible - allow "locking" on specific building (build prerequisites and then building itself)
 
-	//Set of buildings for different goals. Does not include any prerequisites.
-	const BuildingID essential[] = {BuildingID::TAVERN, BuildingID::TOWN_HALL};
-	const BuildingID goldSource[] = {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL};
-	const BuildingID unitsSource[] = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3,
-		BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7};
-	const BuildingID unitsUpgrade[] = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP,
-		BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP};
-	const BuildingID unitGrowth[] = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::HORDE_1,
-		BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR};
-	const BuildingID spells[] = {BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3,
-		BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5};
-	const BuildingID extra[] = {BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3,
-		BuildingID::SPECIAL_4, BuildingID::SHIPYARD}; // all remaining buildings
-
 	TResources currentRes = cb->getResourceAmount();
 	TResources income = estimateIncome();
 
@@ -1147,6 +1204,7 @@ bool VCAI::canRecruitAnyHero (const CGTownInstance * t) const
 
 void VCAI::wander(HeroPtr h)
 {
+	TimeCheck tc("looking for wander destination");
 	while(1)
 	{
 		validateVisitableObjs();
@@ -1164,6 +1222,9 @@ void VCAI::wander(HeroPtr h)
 
 		if(!dests.size())
 		{
+			if (cb->getVisitableObjs(h->visitablePos()).size() > 1)
+				moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate
+
 			auto compareReinforcements = [h](const CGTownInstance *lhs, const CGTownInstance *rhs) -> bool
 	        {
 				return howManyReinforcementsCanGet(h, lhs) < howManyReinforcementsCanGet(h, rhs);
@@ -1189,20 +1250,22 @@ void VCAI::wander(HeroPtr h)
 			else if(townsNotReachable.size())
 			{
 				boost::sort(townsNotReachable, compareReinforcements);
-	            //TODO pick the truly best
-	            const CGTownInstance *t = townsNotReachable.back();
-                logAi->debugStream() << boost::format("%s can't reach any town, we'll try to make our way to %s at %s") % h->name % t->name % t->visitablePos();
+				//TODO pick the truly best
+				const CGTownInstance *t = townsNotReachable.back();
+				logAi->debugStream() << boost::format("%s can't reach any town, we'll try to make our way to %s at %s") % h->name % t->name % t->visitablePos();
 				int3 pos1 = h->pos;
-				striveToGoal(sptr(Goals::VisitTile(t->visitablePos()).sethero(h)));
+				striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h)));
+				//if out hero is stuck, we may need to request another hero to clear the way we see
+
 				if (pos1 == h->pos && h == primaryHero()) //hero can't move
 				{
 					if (canRecruitAnyHero(t))
 						recruitHero(t);
 				}
-	            break;
+				break;
 			}
 			else if(cb->getResourceAmount(Res::GOLD) >= HERO_GOLD_COST)
-	        {
+			{
 				std::vector<const CGTownInstance *> towns = cb->getTownsInfo();
 				erase_if(towns, [](const CGTownInstance *t) -> bool
 				{
@@ -1212,13 +1275,13 @@ void VCAI::wander(HeroPtr h)
 					return false;
 				});
 				boost::sort(towns, compareArmyStrength);
-	            if(towns.size())
-	                    recruitHero(towns.back());
-	            break;
-	        }
-            else
-            {
-                logAi->debugStream() << "Nowhere more to go...";
+				if(towns.size())
+						recruitHero(towns.back());
+				break;
+			}
+			else
+			{
+				logAi->debugStream() << "Nowhere more to go...";
 				break;
 			}
 		}
@@ -1233,10 +1296,6 @@ void VCAI::wander(HeroPtr h)
 			else
 			{
                 logAi->debugStream() << boost::format("Hero %s apparently used all MPs (%d left)") % h->name % h->movement;
-				reserveObject(h, dest); //reserve that object - we predict it will be reached soon
-
-				//removed - do not forget abstract goal so easily
-				//setGoal(h, CGoal(VISIT_TILE).sethero(h).settile(dest->visitablePos()));
 			}
 			break;
 		}
@@ -1520,9 +1579,15 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 		//BNLOG("Hero %s moved from %s to %s at %s", h->name % startHpos % visitedObject->hoverName % h->visitablePos());
 		//throw goalFulfilledException (CGoal(GET_OBJ).setobjid(visitedObject->id));
 	}
-
 	if(h) //we could have lost hero after last move
 	{
+		if (!ret) //reserve object we are heading towards
+		{
+			auto obj = frontOrNull(cb->getVisitableObjs(dst));
+			if (obj)
+				reserveObject(h, obj);
+		}
+
 		cb->recalculatePaths();
 		if (startHpos == h->visitablePos() && !ret) //we didn't move and didn't reach the target
 		{
@@ -2023,9 +2088,11 @@ int3 VCAI::explorationBestNeighbour(int3 hpos, int radius, HeroPtr h)
 	throw cannotFulfillGoalException("No neighbour will bring new discoveries!");
 }
 
-int3 VCAI::explorationNewPoint(int radius, HeroPtr h, std::vector<std::vector<int3> > &tiles)
+int3 VCAI::explorationNewPoint(int radius, HeroPtr h, bool breakUnsafe)
 {
     logAi->debugStream() << "Looking for an another place for exploration...";
+
+	std::vector<std::vector<int3> > tiles; //tiles[distance_to_fow]
 	tiles.resize(radius);
 
 	foreach_tile_pos([&](const int3 &pos)
@@ -2034,6 +2101,9 @@ int3 VCAI::explorationNewPoint(int radius, HeroPtr h, std::vector<std::vector<in
 			tiles[0].push_back(pos);
 	});
 
+	int bestValue = 0;
+	int3 bestTile(-1,-1,-1);
+
 	for (int i = 1; i < radius; i++)
 	{
 		getVisibleNeighbours(tiles[i-1], tiles[i]);
@@ -2043,15 +2113,18 @@ int3 VCAI::explorationNewPoint(int radius, HeroPtr h, std::vector<std::vector<in
 		{
 			if (cb->getTile(tile)->blocked) //does it shorten the time?
 				continue;
-			if(cb->getPathInfo(tile)->reachable() && howManyTilesWillBeDiscovered(tile, radius) &&
-				isSafeToVisit(h, tile) && !isBlockedBorderGate(tile))
+			int ourValue = howManyTilesWillBeDiscovered(tile, radius);
+			if (ourValue > bestValue) //avoid costly checks of tiles that don't reveal much
 			{
-				return tile; //return first tile that will discover anything
+				if(cb->getPathInfo(tile)->reachable()  && (isSafeToVisit(h, tile) || breakUnsafe) && !isBlockedBorderGate(tile))
+				{
+					bestTile = tile; //return first tile that will discover anything
+					bestValue = ourValue;
+				}
 			}
 		}
 	}
-	return int3 (-1,-1,-1);
-	//throw cannotFulfillGoalException("No accessible tile will bring discoveries!");
+	return bestTile;
 }
 
 TResources VCAI::estimateIncome() const
@@ -2125,8 +2198,17 @@ void VCAI::recruitHero(const CGTownInstance * t, bool throwing)
 {
     logAi->debugStream() << boost::format("Trying to recruit a hero in %s at %s") % t->name % t->visitablePos();
 
-	if(auto availableHero = frontOrNull(cb->getAvailableHeroes(t)))
-		cb->recruitHero(t, availableHero);
+	auto heroes = cb->getAvailableHeroes(t);
+	if(heroes.size())
+	{
+		auto hero = heroes[0];
+		if (heroes.size() >= 2) //makes sense to recruit two heroes with starting amries in first week
+		{
+			if (heroes[1]->getTotalStrength() > hero->getTotalStrength())
+				hero = heroes[1];
+		}
+		cb->recruitHero(t, hero);
+	}
 	else if(throwing)
 		throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName());
 }
@@ -2496,6 +2578,9 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj)
 {
 	switch (obj->ID)
 	{	
+		case Obj::TOWN:
+			return obj->tempOwner != h->tempOwner; //do not visit our towns at random
+			break;
 		case Obj::BORDER_GATE:
 		{
 			for (auto q : ai->myCb->getMyQuests())

+ 19 - 3
AI/VCAI/VCAI.h

@@ -8,6 +8,7 @@
 
 #include "../../lib/CThreadHelper.h"
 
+#include "../../lib/GameConstants.h"
 #include "../../lib/VCMI_Lib.h"
 #include "../../lib/CBuildingHandler.h"
 #include "../../lib/CCreatureHandler.h"
@@ -109,19 +110,34 @@ struct SectorMap
 	int3 firstTileToGet(HeroPtr h, crint3 dst); //if h wants to reach tile dst, which tile he should visit to clear the way?
 };
 
+//Set of buildings for different goals. Does not include any prerequisites.
+const BuildingID essential[] = {BuildingID::TAVERN, BuildingID::TOWN_HALL};
+const BuildingID goldSource[] = {BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL};
+const BuildingID unitsSource[] = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3,
+	BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7};
+const BuildingID unitsUpgrade[] = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP,
+	BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP};
+const BuildingID unitGrowth[] = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::HORDE_1,
+	BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR};
+const BuildingID spells[] = {BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3,
+	BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5};
+const BuildingID extra[] = {BuildingID::RESOURCE_SILO, BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3,
+	BuildingID::SPECIAL_4, BuildingID::SHIPYARD}; // all remaining buildings
 
 class VCAI : public CAdventureAI
 {
+public:
 	//internal methods for town development
 
 	//try build an unbuilt structure in maxDays at most (0 = indefinite)
+	/*bool canBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays=7);*/
 	bool tryBuildStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays=7);
 	//try build ANY unbuilt structure
+	BuildingID canBuildAnyStructure(const CGTownInstance * t, std::vector<BuildingID> buildList, unsigned int maxDays=7);
 	bool tryBuildAnyStructure(const CGTownInstance * t, std::vector<BuildingID> buildList, unsigned int maxDays=7);
 	//try build first unbuilt structure
 	bool tryBuildNextStructure(const CGTownInstance * t, std::vector<BuildingID> buildList, unsigned int maxDays=7);
 
-public:
 	friend class FuzzyHelper;
 
 	std::map<const CGObjectInstance *, const CGObjectInstance *> knownSubterraneanGates;
@@ -160,7 +176,7 @@ public:
 	void tryRealize(Goals::AbstractGoal & g);
 
 	int3 explorationBestNeighbour(int3 hpos, int radius, HeroPtr h);
-	int3 explorationNewPoint(int radius, HeroPtr h, std::vector<std::vector<int3> > &tiles);
+	int3 explorationNewPoint(int radius, HeroPtr h, bool breakUnsafe = false);
 	void recruitHero();
 
 	virtual std::string getBattleAIName() const override;
@@ -259,7 +275,7 @@ public:
 
 	void addVisitableObj(const CGObjectInstance *obj);
 	void markObjectVisited (const CGObjectInstance *obj);
-	void reserveObject (HeroPtr h, const CGObjectInstance *obj);
+	void reserveObject (HeroPtr h, const CGObjectInstance *obj); //TODO: reserve all objects that heroes attempt to visit
 	//void removeVisitableObj(const CGObjectInstance *obj);
 	void validateObject(const CGObjectInstance *obj); //checks if object is still visible and if not, removes references to it
 	void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it