mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	Nullkiller: rework defence a bit
This commit is contained in:
		
				
					committed by
					
						 Andrii Danylchenko
						Andrii Danylchenko
					
				
			
			
				
	
			
			
			
						parent
						
							1096f2e9e6
						
					
				
				
					commit
					84e5e6ac17
				
			| @@ -31,7 +31,7 @@ const int ACTUAL_RESOURCE_COUNT = 7; | ||||
| const int ALLOWED_ROAMING_HEROES = 8; | ||||
|  | ||||
| //implementation-dependent | ||||
| extern const double SAFE_ATTACK_CONSTANT; | ||||
| extern const float SAFE_ATTACK_CONSTANT; | ||||
| extern const int GOLD_RESERVE; | ||||
|  | ||||
| //provisional class for AI to store a reference to an owned hero object | ||||
|   | ||||
| @@ -39,11 +39,6 @@ Goals::TGoalVec DefenceBehavior::getTasks() | ||||
|  | ||||
| 	if(heroes.size()) | ||||
| 	{ | ||||
| 		auto mainArmy = *vstd::maxElementByFun(heroes, [](const CGHeroInstance * hero) -> uint64_t | ||||
| 		{ | ||||
| 			return hero->getTotalStrength(); | ||||
| 		}); | ||||
|  | ||||
| 		for(auto town : cb->getTownsInfo()) | ||||
| 		{ | ||||
| 			evaluateDefence(tasks, town); | ||||
| @@ -53,13 +48,41 @@ Goals::TGoalVec DefenceBehavior::getTasks() | ||||
| 	return tasks; | ||||
| } | ||||
|  | ||||
| uint64_t townArmyIncome(const CGTownInstance * town) | ||||
| { | ||||
| 	uint64_t result = 0; | ||||
|  | ||||
| 	for(auto creatureInfo : town->creatures) | ||||
| 	{ | ||||
| 		if(creatureInfo.second.empty()) | ||||
| 			continue; | ||||
|  | ||||
| 		auto creature = creatureInfo.second.back().toCreature(); | ||||
| 		result += creature->AIValue * town->getGrowthInfo(creature->level).totalGrowth(); | ||||
| 	} | ||||
|  | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) | ||||
| { | ||||
| 	logAi->debug("Evaluating defence for %s", town->name); | ||||
| 	auto basicPriority = 0.3f + std::sqrt(townArmyIncome(town) / 20000.0f) | ||||
| 		+ town->dailyIncome()[Res::GOLD] / 10000.0f; | ||||
|  | ||||
| 	logAi->debug("Evaluating defence for %s, basic priority %f", town->name, basicPriority); | ||||
|  | ||||
| 	auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); | ||||
| 	auto treats = { treatNode.fastestDanger, treatNode.maximumDanger }; | ||||
|  | ||||
| 	if(!treatNode.fastestDanger.hero) | ||||
| 	{ | ||||
| 		logAi->debug("No treat found for town %s", town->name); | ||||
|  | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); | ||||
|  | ||||
| 	if(town->garrisonHero) | ||||
| 	{ | ||||
| 		if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get())) | ||||
| @@ -75,17 +98,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta | ||||
|  | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	if(town->visitingHero && isSafeToVisit(town->visitingHero.get(), treatNode.maximumDanger.danger)) | ||||
| 	{ | ||||
| 		logAi->debug( | ||||
| 			"Town %s has visiting hero %s who is strong enough to defend the town",  | ||||
| 			town->name,  | ||||
| 			town->visitingHero->name); | ||||
|  | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	 | ||||
| 	uint64_t reinforcement = ai->ah->howManyReinforcementsCanBuy(town->getUpperArmy(), town); | ||||
|  | ||||
| 	if(reinforcement) | ||||
| @@ -103,44 +116,68 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	for(AIPath & path : paths) | ||||
| 	{ | ||||
| 		for(auto & treat : treats) | ||||
| 		{ | ||||
| 			if(isSafeToVisit(path.targetHero, path.heroArmy, treat.danger)) | ||||
| 			{ | ||||
| 				logAi->debug( | ||||
| 					"Hero %s can eliminate danger for town %s using path %s.",  | ||||
| 					path.targetHero->name, | ||||
| 					town->name, | ||||
| 					path.toString()); | ||||
|  | ||||
| 				return; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for(auto & treat : treats) | ||||
| 	{ | ||||
| 		logAi->debug( | ||||
| 			"Town %s has treat %lld in %s turns, hero: %s",  | ||||
| 			"Town %s has treat %lld in %s turns, hero: %s", | ||||
| 			town->name, | ||||
| 			treat.danger, | ||||
| 			std::to_string(treat.turn), | ||||
| 			treat.hero->name); | ||||
|  | ||||
| 		bool treatIsUnderControl = false; | ||||
|  | ||||
| 		for(AIPath & path : paths) | ||||
| 		{ | ||||
| 			if(path.getHeroStrength() > treat.danger) | ||||
| 			{ | ||||
| 				if(dayOfWeek + treat.turn < 6 && isSafeToVisit(path.targetHero, path.heroArmy, treat.danger) | ||||
| 					|| path.exchangeCount == 1 && path.turn() < treat.turn | ||||
| 					|| path.turn() < treat.turn - 1) | ||||
| 				{ | ||||
| 					logAi->debug( | ||||
| 						"Hero %s can eliminate danger for town %s using path %s.", | ||||
| 						path.targetHero->name, | ||||
| 						town->name, | ||||
| 						path.toString()); | ||||
|  | ||||
| 					treatIsUnderControl = true; | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if(treatIsUnderControl) | ||||
| 			continue; | ||||
|  | ||||
| 		if(ai->canRecruitAnyHero(town)) | ||||
| 		{ | ||||
| 			auto heroesInTavern = cb->getAvailableHeroes(town); | ||||
|  | ||||
| 			for(auto hero : heroesInTavern) | ||||
| 			{ | ||||
| 				if(hero->getTotalStrength() > treat.danger) | ||||
| 				{ | ||||
| 					tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town).setobjid(hero->id.getNum()).setpriority(1))); | ||||
| 					continue; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		for(AIPath & path : paths) | ||||
| 		{ | ||||
| #if AI_TRACE_LEVEL >= 1 | ||||
| 			logAi->trace( | ||||
| 				"Hero %s can defend town with force %lld in %s turns, path: %s", | ||||
| 				"Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s", | ||||
| 				path.targetHero->name, | ||||
| 				path.getHeroStrength(), | ||||
| 				std::to_string(path.turn()), | ||||
| 				path.movementCost(), | ||||
| 				path.toString()); | ||||
| #endif | ||||
|  | ||||
| 			float priority = 0.6f + (float)path.getHeroStrength() / treat.danger / (treat.turn + 1); | ||||
| 			float priority = basicPriority | ||||
| 				+ std::min(SAFE_ATTACK_CONSTANT, (float)path.getHeroStrength() / treat.danger) / (treat.turn + 1); | ||||
|  | ||||
| 			if(path.targetHero == town->visitingHero && path.exchangeCount == 1) | ||||
| 			{ | ||||
| @@ -156,7 +193,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta | ||||
| 				continue; | ||||
| 			} | ||||
| 				 | ||||
| 			if(path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger) | ||||
| 			if(path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger) | ||||
| 			{ | ||||
| #if AI_TRACE_LEVEL >= 1 | ||||
| 				logAi->trace("Move %s to defend town %s with priority %f", | ||||
|   | ||||
| @@ -31,13 +31,17 @@ std::string RecruitHeroBehavior::toString() const | ||||
| Goals::TGoalVec RecruitHeroBehavior::getTasks() | ||||
| { | ||||
| 	Goals::TGoalVec tasks; | ||||
| 	auto towns = cb->getTownsInfo(); | ||||
|  | ||||
| 	if(ai->canRecruitAnyHero()) | ||||
| 	for(auto town : towns) | ||||
| 	{ | ||||
| 		if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 | ||||
| 			|| cb->getResourceAmount(Res::GOLD) > 10000) | ||||
| 		if(!town->garrisonHero && ai->canRecruitAnyHero(town)) | ||||
| 		{ | ||||
| 			tasks.push_back(Goals::sptr(Goals::RecruitHero())); | ||||
| 			if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 | ||||
| 				|| cb->getResourceAmount(Res::GOLD) > 10000) | ||||
| 			{ | ||||
| 				tasks.push_back(Goals::sptr(Goals::RecruitHero().settown(town))); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -66,16 +66,37 @@ void ExchangeSwapTownHeroes::accept(VCAI * ai) | ||||
| 	if(town->visitingHero && town->visitingHero.get() != garrisonHero) | ||||
| 		cb->swapGarrisonHero(town); | ||||
|  | ||||
| 	makePossibleUpgrades(town); | ||||
| 	ai->moveHeroToTile(town->visitablePos(), garrisonHero); | ||||
|  | ||||
| 	cb->swapGarrisonHero(town); // selected hero left in garrison with strongest army | ||||
| 	ai->nullkiller->lockHero(town->garrisonHero.get()); | ||||
| 	auto upperArmy = town->getUpperArmy(); | ||||
| 	 | ||||
| 	if(!town->garrisonHero && upperArmy->stacksCount() != 0) | ||||
| 	{ | ||||
| 		// dismiss creatures we are not able to pick to be able to hide in garrison | ||||
| 		if(upperArmy->getArmyStrength() < 500  | ||||
| 			&& town->fortLevel() >= CGTownInstance::CITADEL) | ||||
| 		{ | ||||
| 			for(auto slot : upperArmy->Slots()) | ||||
| 			{ | ||||
| 				cb->dismissCreature(upperArmy, slot.first); | ||||
| 			} | ||||
|  | ||||
| 	if(town->visitingHero) | ||||
| 			cb->swapGarrisonHero(town); | ||||
| 		} | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		cb->swapGarrisonHero(town); // selected hero left in garrison with strongest army | ||||
| 	} | ||||
|  | ||||
| 	ai->nullkiller->lockHero(garrisonHero); | ||||
|  | ||||
| 	if(town->visitingHero && town->visitingHero != garrisonHero) | ||||
| 	{ | ||||
| 		ai->nullkiller->unlockHero(town->visitingHero.get()); | ||||
| 		makePossibleUpgrades(town->visitingHero); | ||||
| 	} | ||||
|  | ||||
| 	logAi->debug("Put hero %s to garrison of %s", town->garrisonHero->name, town->name); | ||||
| 	logAi->debug("Put hero %s to garrison of %s", garrisonHero->name, town->name); | ||||
| } | ||||
| @@ -96,9 +96,36 @@ void ExecuteHeroChain::accept(VCAI * ai) | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if(node.turns == 0) | ||||
| 				{ | ||||
| 					auto targetNode = cb->getPathsInfo(hero.get())->getPathInfo(node.coord); | ||||
|  | ||||
| 					if(!targetNode->accessible || targetNode->turns != 0) | ||||
| 					{ | ||||
| 						logAi->error( | ||||
| 							"Enable to complete chain. Expected hero %s to arive to %s but he in 0 turns but he can not do this", | ||||
| 							hero.name, | ||||
| 							node.coord.toString(), | ||||
| 							hero->visitablePos().toString()); | ||||
|  | ||||
| 						return; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				Goals::VisitTile(node.coord).sethero(hero).accept(ai); | ||||
| 			} | ||||
|  | ||||
| 			if(node.turns == 0) | ||||
| 			{ | ||||
| 				logAi->error( | ||||
| 					"Enable to complete chain. Expected hero %s to arive to %s but he is at %s",  | ||||
| 					hero.name,  | ||||
| 					node.coord.toString(), | ||||
| 					hero->visitablePos().toString()); | ||||
|  | ||||
| 				return; | ||||
| 			} | ||||
|  | ||||
| 			// no exception means we were not able to rich the tile | ||||
| 			ai->nullkiller->lockHero(hero.get()); | ||||
| 			blockedIndexes.insert(node.parentIndex); | ||||
|   | ||||
| @@ -190,17 +190,6 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf | ||||
| 		{ | ||||
| 			dstNode->specialAction->applyOnDestination(dstNode->actor->hero, destination, source, dstNode, srcNode); | ||||
| 		} | ||||
|  | ||||
| #if AI_TRACE_LEVEL >= 2 | ||||
| 		logAi->trace( | ||||
| 			"Commited %s -> %s, cost: %f, hero: %s, mask: %x, army: %i",  | ||||
| 			source.coord.toString(), | ||||
| 			destination.coord.toString(), | ||||
| 			destination.cost, | ||||
| 			dstNode->actor->toString(), | ||||
| 			dstNode->actor->chainMask, | ||||
| 			dstNode->actor->armyValue); | ||||
| #endif | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| @@ -212,6 +201,11 @@ void AINodeStorage::commit( | ||||
| 	int movementLeft,  | ||||
| 	float cost) const | ||||
| { | ||||
| 	if(destination->actor->chainMask == 195 && turn == 0) | ||||
| 	{ | ||||
| 		throw std::exception(); | ||||
| 	} | ||||
|  | ||||
| 	destination->action = action; | ||||
| 	destination->cost = cost; | ||||
| 	destination->moveRemains = movementLeft; | ||||
| @@ -221,6 +215,19 @@ void AINodeStorage::commit( | ||||
| 	destination->danger = source->danger; | ||||
| 	destination->theNodeBefore = source->theNodeBefore; | ||||
| 	destination->chainOther = nullptr; | ||||
|  | ||||
| #if AI_TRACE_LEVEL >= 2 | ||||
| 	logAi->trace( | ||||
| 		"Commited %s -> %s, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld", | ||||
| 		source->coord.toString(), | ||||
| 		destination->coord.toString(), | ||||
| 		destination->cost, | ||||
| 		std::to_string(destination->turns), | ||||
| 		destination->moveRemains, | ||||
| 		destination->actor->toString(), | ||||
| 		destination->actor->chainMask, | ||||
| 		destination->actor->armyValue); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| std::vector<CGPathNode *> AINodeStorage::calculateNeighbours( | ||||
| @@ -318,7 +325,7 @@ void AINodeStorage::calculateHeroChain( | ||||
|  | ||||
| #if AI_TRACE_LEVEL >= 2 | ||||
| 		logAi->trace( | ||||
| 			"Thy exchange %s[%i] -> %s[%i] at %s", | ||||
| 			"Thy exchange %s[%x] -> %s[%x] at %s", | ||||
| 			node->actor->toString(), | ||||
| 			node->actor->chainMask, | ||||
| 			srcNode->actor->toString(), | ||||
| @@ -343,7 +350,7 @@ void AINodeStorage::calculateHeroChain( | ||||
| 	{ | ||||
| #if AI_TRACE_LEVEL >= 2 | ||||
| 		logAi->trace( | ||||
| 			"Exchange allowed %s[%i] -> %s[%i] at %s", | ||||
| 			"Exchange allowed %s[%x] -> %s[%x] at %s", | ||||
| 			other->actor->toString(), | ||||
| 			other->actor->chainMask, | ||||
| 			carrier->actor->toString(), | ||||
| @@ -423,12 +430,14 @@ void AINodeStorage::addHeroChain(const std::vector<ExchangeCandidate> & result) | ||||
|  | ||||
| #if AI_TRACE_LEVEL >= 2 | ||||
| 		logAi->trace( | ||||
| 			"Chain accepted at %s %s -> %s, mask %x, cost %f, army %i",  | ||||
| 			"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(), | ||||
| 			exchangeNode->actor->chainMask, | ||||
| 			exchangeNode->cost, | ||||
| 			std::to_string(exchangeNode->turns), | ||||
| 			exchangeNode->moveRemains, | ||||
| 			exchangeNode->actor->armyValue); | ||||
| #endif | ||||
| 		heroChain.push_back(exchangeNode); | ||||
| @@ -581,16 +590,110 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations( | ||||
| 	return neighbours; | ||||
| } | ||||
|  | ||||
| void AINodeStorage::getBestInitialNodeForTownPortal() | ||||
| struct TowmPortalFinder | ||||
| { | ||||
| 	const std::vector<CGPathNode *> & initialNodes; | ||||
| 	SecSkillLevel::SecSkillLevel townPortalSkillLevel; | ||||
| 	uint64_t movementNeeded; | ||||
| 	const ChainActor * actor; | ||||
| 	const CGHeroInstance * hero; | ||||
| 	std::vector<const CGTownInstance *> targetTowns; | ||||
| 	AINodeStorage * nodeStorage; | ||||
|  | ||||
| } | ||||
| 	SpellID spellID; | ||||
| 	const CSpell * townPortal; | ||||
|  | ||||
| 	TowmPortalFinder( | ||||
| 		const ChainActor * actor, | ||||
| 		const std::vector<CGPathNode *> & initialNodes, | ||||
| 		std::vector<const CGTownInstance *> targetTowns, | ||||
| 		AINodeStorage * nodeStorage) | ||||
| 		:actor(actor), initialNodes(initialNodes), hero(actor->hero), | ||||
| 		targetTowns(targetTowns), nodeStorage(nodeStorage) | ||||
| 	{ | ||||
| 		spellID = SpellID::TOWN_PORTAL; | ||||
| 		townPortal = spellID.toSpell(); | ||||
|  | ||||
| 		// TODO: Copy/Paste from TownPortalMechanics | ||||
| 		townPortalSkillLevel = SecSkillLevel::SecSkillLevel(hero->getSpellSchoolLevel(townPortal)); | ||||
| 		movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= SecSkillLevel::EXPERT ? 2 : 3); | ||||
| 	} | ||||
|  | ||||
| 	bool actorCanCastTownPortal() | ||||
| 	{ | ||||
| 		return hero->canCastThisSpell(townPortal) && hero->mana >= hero->getSpellCost(townPortal); | ||||
| 	} | ||||
|  | ||||
| 	CGPathNode * getBestInitialNodeForTownPortal(const CGTownInstance * targetTown) | ||||
| 	{ | ||||
| 		CGPathNode * bestNode = nullptr; | ||||
|  | ||||
| 		for(CGPathNode * node : initialNodes) | ||||
| 		{ | ||||
| 			auto aiNode = nodeStorage->getAINode(node); | ||||
|  | ||||
| 			if(aiNode->actor->baseActor != actor | ||||
| 				|| node->layer != EPathfindingLayer::LAND | ||||
| 				|| node->moveRemains < movementNeeded) | ||||
| 			{ | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			if(townPortalSkillLevel < SecSkillLevel::ADVANCED) | ||||
| 			{ | ||||
| 				const CGTownInstance * nearestTown = *vstd::minElementByFun(targetTowns, [&](const CGTownInstance * t) -> int | ||||
| 				{ | ||||
| 					return node->coord.dist2dSQ(t->visitablePos()); | ||||
| 				}); | ||||
|  | ||||
| 				if(targetTown != nearestTown) | ||||
| 					continue; | ||||
| 			} | ||||
|  | ||||
| 			if(!bestNode || bestNode->cost > node->cost) | ||||
| 				bestNode = node; | ||||
| 		} | ||||
|  | ||||
| 		return bestNode; | ||||
| 	} | ||||
|  | ||||
| 	boost::optional<AIPathNode *> createTownPortalNode(const CGTownInstance * targetTown) | ||||
| 	{ | ||||
| 		auto bestNode = getBestInitialNodeForTownPortal(targetTown); | ||||
|  | ||||
| 		if(!bestNode) | ||||
| 			return boost::none; | ||||
|  | ||||
| 		auto nodeOptional = nodeStorage->getOrCreateNode(targetTown->visitablePos(), EPathfindingLayer::LAND, actor->castActor); | ||||
|  | ||||
| 		if(!nodeOptional) | ||||
| 			return boost::none; | ||||
|  | ||||
| 		AIPathNode * node = nodeOptional.get(); | ||||
| 		float movementCost = (float)movementNeeded / (float)hero->maxMovePoints(EPathfindingLayer::LAND); | ||||
|  | ||||
| 		movementCost += bestNode->cost; | ||||
|  | ||||
| 		if(node->action == CGPathNode::UNKNOWN || node->cost > movementCost) | ||||
| 		{ | ||||
| 			nodeStorage->commit( | ||||
| 				node, | ||||
| 				nodeStorage->getAINode(bestNode), | ||||
| 				CGPathNode::TELEPORT_NORMAL, | ||||
| 				bestNode->turns, | ||||
| 				bestNode->moveRemains - movementNeeded, | ||||
| 				movementCost); | ||||
|  | ||||
| 			node->theNodeBefore = bestNode; | ||||
| 			node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown)); | ||||
| 		} | ||||
|  | ||||
| 		return nodeOptional; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *> & initialNodes) | ||||
| { | ||||
| 	SpellID spellID = SpellID::TOWN_PORTAL; | ||||
| 	const CSpell * townPortal = spellID.toSpell(); | ||||
|  | ||||
| 	std::set<const ChainActor *> actorsOfInitial; | ||||
|  | ||||
| 	for(const CGPathNode * node : initialNodes) | ||||
| @@ -605,92 +708,36 @@ void AINodeStorage::calculateTownPortalTeleportations(std::vector<CGPathNode *> | ||||
| 		if(!actor->hero) | ||||
| 			continue; | ||||
|  | ||||
| 		auto hero = actor->hero; | ||||
| 		auto towns = cb->getTownsInfo(false); | ||||
|  | ||||
| 		if(hero->canCastThisSpell(townPortal) && hero->mana >= hero->getSpellCost(townPortal)) | ||||
| 		vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool | ||||
| 		{ | ||||
| 			auto towns = cb->getTownsInfo(false); | ||||
| 			return cb->getPlayerRelations(actor->hero->tempOwner, t->tempOwner) == PlayerRelations::ENEMIES; | ||||
| 		}); | ||||
|  | ||||
| 			vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool | ||||
| 			{ | ||||
| 				return cb->getPlayerRelations(hero->tempOwner, t->tempOwner) == PlayerRelations::ENEMIES; | ||||
| 			}); | ||||
| 		if(!towns.size()) | ||||
| 		{ | ||||
| 			return; // no towns no need to run loop further | ||||
| 		} | ||||
|  | ||||
| 			if(!towns.size()) | ||||
| 			{ | ||||
| 				return; // no towns no need to run loop further | ||||
| 			} | ||||
|  | ||||
| 			// TODO: Copy/Paste from TownPortalMechanics | ||||
| 			auto skillLevel = hero->getSpellSchoolLevel(townPortal); | ||||
| 			auto movementNeeded = GameConstants::BASE_MOVEMENT_COST * (skillLevel >= 3 ? 2 : 3); | ||||
| 		TowmPortalFinder townPortalFinder(actor, initialNodes, towns, this); | ||||
|  | ||||
| 		if(townPortalFinder.actorCanCastTownPortal()) | ||||
| 		{ | ||||
| 			for(const CGTownInstance * targetTown : towns) | ||||
| 			{ | ||||
| 				CGPathNode * bestNode = nullptr; | ||||
|  | ||||
| 				for(CGPathNode * node : initialNodes) | ||||
| 				{ | ||||
| 					auto aiNode = getAINode(node); | ||||
|  | ||||
| 					if(aiNode->actor->baseActor != actor | ||||
| 						|| node->layer != EPathfindingLayer::LAND | ||||
| 						|| node->moveRemains < movementNeeded) | ||||
| 					{ | ||||
| 						continue; | ||||
| 					} | ||||
|  | ||||
| 					if(skillLevel < SecSkillLevel::ADVANCED) | ||||
| 					{ | ||||
| 						const CGTownInstance * nearestTown = *vstd::minElementByFun(towns, [&](const CGTownInstance * t) -> int | ||||
| 						{ | ||||
| 							return node->coord.dist2dSQ(t->visitablePos()); | ||||
| 						}); | ||||
|  | ||||
| 						if(targetTown != nearestTown) | ||||
| 							continue; | ||||
| 					} | ||||
|  | ||||
| 					if(!bestNode || bestNode->cost > node->cost) | ||||
| 						bestNode = node; | ||||
| 				} | ||||
|  | ||||
| 				if(!bestNode) | ||||
| 					continue; | ||||
|  | ||||
| 				// TODO: allow to hide visiting hero in garrison | ||||
| 				if(targetTown->visitingHero && targetTown->visitingHero != hero) | ||||
| 				if(targetTown->visitingHero && targetTown->visitingHero != actor->hero) | ||||
| 					continue; | ||||
|  | ||||
| 				auto nodeOptional = getOrCreateNode(targetTown->visitablePos(), EPathfindingLayer::LAND, actor->castActor); | ||||
| 				auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown); | ||||
|  | ||||
| 				if(nodeOptional) | ||||
| 				{ | ||||
| 					float movementCost = (float)movementNeeded / (float)hero->maxMovePoints(EPathfindingLayer::LAND); | ||||
|  | ||||
| 					movementCost += bestNode->cost; | ||||
|  | ||||
| #ifdef AI_TRACE_LEVEL >= 1 | ||||
| #if AI_TRACE_LEVEL >= 1 | ||||
| 					logAi->trace("Adding town portal node at %s", targetTown->name); | ||||
| #endif | ||||
|  | ||||
| 					AIPathNode * node = nodeOptional.get(); | ||||
|  | ||||
| 					if(node->action == CGPathNode::UNKNOWN || node->cost > movementCost) | ||||
| 					{ | ||||
| 						commit( | ||||
| 							node, | ||||
| 							getAINode(bestNode), | ||||
| 							CGPathNode::TELEPORT_NORMAL, | ||||
| 							bestNode->turns, | ||||
| 							bestNode->moveRemains - movementNeeded, | ||||
| 							movementCost); | ||||
|  | ||||
| 						node->theNodeBefore = bestNode; | ||||
| 						node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown)); | ||||
| 					} | ||||
|  | ||||
| 					initialNodes.push_back(node); | ||||
| 					initialNodes.push_back(nodeOptional.get()); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| @@ -726,20 +773,21 @@ bool AINodeStorage::hasBetterChain( | ||||
| 		{ | ||||
| 			if(node.cost < candidateNode->cost) | ||||
| 			{ | ||||
| #ifdef AI_TRACE_LEVEL >= 1 | ||||
| #if AI_TRACE_LEVEL >= 2 | ||||
| 				logAi->trace( | ||||
| 					"Block ineficient move %s:->%s, mask=%i, mp diff: %i", | ||||
| 					"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i", | ||||
| 					source->coord.toString(), | ||||
| 					candidateNode->coord.toString(), | ||||
| 					candidateNode->actor->hero->name, | ||||
| 					candidateNode->actor->chainMask, | ||||
| 					candidateNode->actor->armyValue, | ||||
| 					node.moveRemains - candidateNode->moveRemains); | ||||
| #endif | ||||
| 				return true; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if(candidateActor->actorExchangeCount == 1 | ||||
| 			&& (candidateActor->chainMask & node.actor->chainMask) == 0) | ||||
| 		if((candidateActor->chainMask & node.actor->chainMask) == 0) | ||||
| 			continue; | ||||
|  | ||||
| 		auto nodeActor = node.actor; | ||||
| @@ -749,15 +797,42 @@ bool AINodeStorage::hasBetterChain( | ||||
| 		if(nodeArmyValue > candidateArmyValue | ||||
| 			&& node.cost <= candidateNode->cost) | ||||
| 		{ | ||||
| #if AI_TRACE_LEVEL >= 2 | ||||
| 			logAi->trace( | ||||
| 				"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i", | ||||
| 				source->coord.toString(), | ||||
| 				candidateNode->coord.toString(), | ||||
| 				candidateNode->actor->hero->name, | ||||
| 				candidateNode->actor->chainMask, | ||||
| 				candidateNode->actor->armyValue, | ||||
| 				node.moveRemains - candidateNode->moveRemains); | ||||
| #endif | ||||
| 			return true; | ||||
| 		} | ||||
|  | ||||
| 		if(nodeArmyValue == candidateArmyValue | ||||
| 		/*if(nodeArmyValue == candidateArmyValue | ||||
| 			&& nodeActor->heroFightingStrength >= candidateActor->heroFightingStrength | ||||
| 			&& node.cost <= candidateNode->cost) | ||||
| 		{ | ||||
| 			if(nodeActor->heroFightingStrength == candidateActor->heroFightingStrength | ||||
| 				&& node.cost == candidateNode->cost | ||||
| 				&& &node < candidateNode) | ||||
| 			{ | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| #if AI_TRACE_LEVEL >= 2 | ||||
| 			logAi->trace( | ||||
| 				"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i", | ||||
| 				source->coord.toString(), | ||||
| 				candidateNode->coord.toString(), | ||||
| 				candidateNode->actor->hero->name, | ||||
| 				candidateNode->actor->chainMask, | ||||
| 				candidateNode->actor->armyValue, | ||||
| 				node.moveRemains - candidateNode->moveRemains); | ||||
| #endif | ||||
| 			return true; | ||||
| 		} | ||||
| 		}*/ | ||||
| 	} | ||||
|  | ||||
| 	return false; | ||||
| @@ -821,11 +896,12 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa | ||||
| 		if(node->chainOther) | ||||
| 			fillChainInfo(node->chainOther, path, parentIndex); | ||||
|  | ||||
| 		if(node->actor->hero->visitablePos() != node->coord) | ||||
| 		//if(node->actor->hero->visitablePos() != node->coord) | ||||
| 		{ | ||||
| 			AIPathNodeInfo pathNode; | ||||
| 			pathNode.cost = node->cost; | ||||
| 			pathNode.targetHero = node->actor->hero; | ||||
| 			pathNode.chainMask = node->actor->chainMask; | ||||
| 			pathNode.specialAction = node->specialAction; | ||||
| 			pathNode.turns = node->turns; | ||||
| 			pathNode.danger = node->danger; | ||||
| @@ -862,7 +938,7 @@ int3 AIPath::targetTile() const | ||||
| { | ||||
| 	if(nodes.size()) | ||||
| 	{ | ||||
| 		return nodes.front().coord; | ||||
| 		return targetNode().coord; | ||||
| 	} | ||||
|  | ||||
| 	return int3(-1, -1, -1); | ||||
| @@ -873,36 +949,35 @@ const AIPathNodeInfo & AIPath::firstNode() const | ||||
| 	return nodes.back(); | ||||
| } | ||||
|  | ||||
| const AIPathNodeInfo & AIPath::targetNode() const | ||||
| { | ||||
| 	auto & node = nodes.front(); | ||||
|  | ||||
| 	return targetHero == node.targetHero ? node : nodes.at(1); | ||||
| } | ||||
|  | ||||
| uint64_t AIPath::getPathDanger() const | ||||
| { | ||||
| 	if(nodes.size()) | ||||
| 	{ | ||||
| 		return nodes.front().danger; | ||||
| 	} | ||||
| 	if(nodes.empty()) | ||||
| 		return 0; | ||||
|  | ||||
| 	return 0; | ||||
| 	return targetNode().danger; | ||||
| } | ||||
|  | ||||
| float AIPath::movementCost() const | ||||
| { | ||||
| 	if(nodes.size()) | ||||
| 	{ | ||||
| 		return nodes.front().cost; | ||||
| 	} | ||||
| 	if(nodes.empty()) | ||||
| 		return 0.0f; | ||||
|  | ||||
| 	// TODO: boost:optional? | ||||
| 	return 0.0; | ||||
| 	return targetNode().cost; | ||||
| } | ||||
|  | ||||
| uint8_t AIPath::turn() const | ||||
| { | ||||
| 	if(nodes.size()) | ||||
| 	{ | ||||
| 		return nodes.front().turns; | ||||
| 	} | ||||
| 	if(nodes.empty()) | ||||
| 		return 0; | ||||
|  | ||||
| 	// TODO: boost:optional? | ||||
| 	return 0; | ||||
| 	return targetNode().turns; | ||||
| } | ||||
|  | ||||
| uint64_t AIPath::getHeroStrength() const | ||||
| @@ -921,9 +996,11 @@ uint64_t AIPath::getTotalDanger(HeroPtr hero) const | ||||
| std::string AIPath::toString() | ||||
| { | ||||
| 	std::stringstream str; | ||||
| 	 | ||||
|  | ||||
| 	str << targetHero->name << "[" << std::hex << chainMask << std::dec << "]" << ": "; | ||||
|  | ||||
| 	for(auto node : nodes) | ||||
| 		str << node.targetHero->name << "->" << node.coord.toString() << "; "; | ||||
| 		str << node.targetHero->name << "[" << std::hex << node.chainMask << std::dec << "]" << "->" << node.coord.toString() << "; "; | ||||
|  | ||||
| 	return str.str(); | ||||
| } | ||||
| @@ -10,8 +10,8 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #define VCMI_TRACE_PATHFINDER 2 | ||||
| #define AI_TRACE_LEVEL 2 | ||||
| #define VCMI_TRACE_PATHFINDER 1 | ||||
| #define AI_TRACE_LEVEL 1 | ||||
|  | ||||
| #include "../../../lib/CPathfinder.h" | ||||
| #include "../../../lib/mapObjects/CGHeroInstance.h" | ||||
| @@ -39,6 +39,7 @@ struct AIPathNodeInfo | ||||
| 	uint64_t danger; | ||||
| 	const CGHeroInstance * targetHero; | ||||
| 	int parentIndex; | ||||
| 	uint64_t chainMask; | ||||
| 	std::shared_ptr<const ISpecialAction> specialAction; | ||||
| }; | ||||
|  | ||||
| @@ -66,6 +67,8 @@ struct AIPath | ||||
|  | ||||
| 	const AIPathNodeInfo & firstNode() const; | ||||
|  | ||||
| 	const AIPathNodeInfo & targetNode() const; | ||||
|  | ||||
| 	float movementCost() const; | ||||
|  | ||||
| 	uint8_t turn() const; | ||||
| @@ -99,7 +102,7 @@ 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. | ||||
| 	static const int NUM_CHAINS = 5 * GameConstants::MAX_HEROES_PER_PLAYER; | ||||
| 	static const int NUM_CHAINS = 10 * GameConstants::MAX_HEROES_PER_PLAYER; | ||||
| 	 | ||||
| 	AINodeStorage(const int3 & sizes); | ||||
| 	~AINodeStorage(); | ||||
| @@ -120,11 +123,28 @@ public: | ||||
|  | ||||
| 	virtual void commit(CDestinationNodeInfo & destination, const PathNodeInfo & source) override; | ||||
|  | ||||
| 	void commit( | ||||
| 		AIPathNode * destination, | ||||
| 		const AIPathNode * source, | ||||
| 		CGPathNode::ENodeAction action, | ||||
| 		int turn, | ||||
| 		int movementLeft, | ||||
| 		float cost) const; | ||||
|  | ||||
| 	const AIPathNode * getAINode(const CGPathNode * node) const; | ||||
| 	void updateAINode(CGPathNode * node, std::function<void (AIPathNode *)> updater); | ||||
|  | ||||
| 	bool hasBetterChain(const PathNodeInfo & source, CDestinationNodeInfo & destination) const; | ||||
|  | ||||
| 	bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const | ||||
| 	{ | ||||
| 		// further chain distribution is calculated as the last stage | ||||
| 		if(heroChainPass && destination.node->turns > heroChainTurn) | ||||
| 			return true; | ||||
|  | ||||
| 		return hasBetterChain(source, destination); | ||||
| 	} | ||||
|  | ||||
| 	template<class NodeRange> | ||||
| 	bool hasBetterChain( | ||||
| 		const CGPathNode * source,  | ||||
| @@ -167,13 +187,6 @@ private: | ||||
|  | ||||
| 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours); | ||||
| 	void fillChainInfo(const AIPathNode * node, AIPath & path, int parentIndex) const; | ||||
| 	void commit( | ||||
| 		AIPathNode * destination,  | ||||
| 		const AIPathNode * source,  | ||||
| 		CGPathNode::ENodeAction action,  | ||||
| 		int turn,  | ||||
| 		int movementLeft,  | ||||
| 		float cost) const; | ||||
|  | ||||
| 	ExchangeCandidate calculateExchange( | ||||
| 		ChainActor * exchangeActor,  | ||||
|   | ||||
| @@ -45,7 +45,7 @@ namespace AIPathfinding | ||||
| 		const PathfinderConfig * pathfinderConfig, | ||||
| 		CPathfinderHelper * pathfinderHelper) const | ||||
| 	{ | ||||
| 		if(nodeStorage->hasBetterChain(source, destination)) | ||||
| 		if(nodeStorage->isMovementIneficient(source, destination)) | ||||
| 		{ | ||||
| 			destination.blocked = true; | ||||
|  | ||||
|   | ||||
| @@ -32,7 +32,7 @@ extern FuzzyHelper * fh; | ||||
|  | ||||
| class CGVisitableOPW; | ||||
|  | ||||
| const double SAFE_ATTACK_CONSTANT = 1.5; | ||||
| const float SAFE_ATTACK_CONSTANT = 1.5; | ||||
|  | ||||
| //one thread may be turn of AI and another will be handling a side effect for AI2 | ||||
| boost::thread_specific_ptr<CCallback> cb; | ||||
| @@ -1064,9 +1064,13 @@ void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h) | ||||
| 		checkHeroArmy(h); | ||||
| 		break; | ||||
| 	case Obj::TOWN: | ||||
| 		moveCreaturesToHero(dynamic_cast<const CGTownInstance *>(obj)); | ||||
| 		if(h->visitedTown) //we are inside, not just attacking | ||||
| 		{ | ||||
| 			makePossibleUpgrades(h.get()); | ||||
|  | ||||
| 			if(!h->visitedTown->garrisonHero) | ||||
| 				moveCreaturesToHero(h->visitedTown); | ||||
|  | ||||
| 			townVisitsThisWeek[h].insert(h->visitedTown); | ||||
| 			ah->updateHeroRoles(); | ||||
| 			if(ah->getHeroRole(h) == HeroRole::MAIN && !h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user