mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	BattleAI: fix bait for archers when need to go long way
This commit is contained in:
		| @@ -213,7 +213,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) | ||||
| 				moveTarget.score, | ||||
| 				moveTarget.scorePerTurn); | ||||
|  | ||||
| 			return goTowardsNearest(stack, moveTarget.positions); | ||||
| 			return goTowardsNearest(stack, moveTarget.positions, *targets); | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| @@ -235,7 +235,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) | ||||
| 			if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) | ||||
| 				return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); | ||||
| 			else | ||||
| 				return goTowardsNearest(stack, brokenWallMoat); | ||||
| 				return goTowardsNearest(stack, brokenWallMoat, *targets); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -249,7 +249,7 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> | ||||
| 	return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count(); | ||||
| } | ||||
|  | ||||
| BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes) | ||||
| BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets) | ||||
| { | ||||
| 	auto reachability = cb->getBattle(battleID)->getReachability(stack); | ||||
| 	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); | ||||
| @@ -272,7 +272,27 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector | ||||
| 		{ | ||||
| 			if(vstd::contains(avHexes, hex)) | ||||
| 			{ | ||||
| 				return BattleAction::makeMove(stack, hex); | ||||
| 				auto additionalScore = 0; | ||||
| 				std::optional<AttackPossibility> attackOnTheWay; | ||||
|  | ||||
| 				for(auto & target : targets.possibleAttacks) | ||||
| 				{ | ||||
| 					if(!target.attack.shooting && target.from == hex && target.attackValue() > additionalScore) | ||||
| 					{ | ||||
| 						additionalScore = target.attackValue(); | ||||
| 						attackOnTheWay = target; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if(attackOnTheWay) | ||||
| 				{ | ||||
| 					activeActionMade = true; | ||||
| 					return BattleAction::makeMeleeAttack(stack, attackOnTheWay->attack.defender->getPosition(), attackOnTheWay->from); | ||||
| 				} | ||||
| 				else | ||||
| 				{ | ||||
| 					return BattleAction::makeMove(stack, hex); | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if(stack->coversPos(hex)) | ||||
|   | ||||
| @@ -43,7 +43,7 @@ public: | ||||
| 	bool attemptCastingSpell(const CStack * stack); | ||||
| 	bool canCastSpell(); | ||||
| 	std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack); | ||||
| 	BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes); | ||||
| 	BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets); | ||||
| 	std::vector<BattleHex> getBrokenWallMoatHexes() const; | ||||
| 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only | ||||
| 	void print(const std::string & text) const; | ||||
|   | ||||
| @@ -277,6 +277,36 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| ReachabilityInfo getReachabilityWithEnemyBypass( | ||||
| 	const battle::Unit * activeStack, | ||||
| 	DamageCache & damageCache, | ||||
| 	std::shared_ptr<HypotheticBattle> state) | ||||
| { | ||||
| 	ReachabilityInfo::Parameters params(activeStack, activeStack->getPosition()); | ||||
|  | ||||
| 	if(!params.flying) | ||||
| 	{ | ||||
| 		for(const auto * unit : state->battleAliveUnits()) | ||||
| 		{ | ||||
| 			if(unit->unitSide() == activeStack->unitSide()) | ||||
| 				continue; | ||||
|  | ||||
| 			auto dmg = damageCache.getOriginalDamage(activeStack, unit, state); | ||||
| 			auto turnsToKill = unit->getAvailableHealth() / dmg + 1; | ||||
|  | ||||
| 			vstd::amin(turnsToKill, 100); | ||||
|  | ||||
| 			for(auto & hex : unit->getHexes()) | ||||
| 				if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns | ||||
| 					params.destructibleEnemyTurns[hex] = turnsToKill * unit->getMovementRange(); | ||||
| 		} | ||||
|  | ||||
| 		params.bypassEnemyStacks = true; | ||||
| 	} | ||||
|  | ||||
| 	return state->getReachability(params); | ||||
| } | ||||
|  | ||||
| MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( | ||||
| 	const battle::Unit * activeStack, | ||||
| 	PotentialTargets & targets, | ||||
| @@ -286,6 +316,8 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( | ||||
| 	MoveTarget result; | ||||
| 	BattleExchangeVariant ev; | ||||
|  | ||||
| 	logAi->trace("Find move towards unreachable. Enemies count %d", targets.unreachableEnemies.size()); | ||||
|  | ||||
| 	if(targets.unreachableEnemies.empty()) | ||||
| 		return result; | ||||
|  | ||||
| @@ -296,17 +328,17 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( | ||||
|  | ||||
| 	updateReachabilityMap(hb); | ||||
|  | ||||
| 	auto dists = cb->getReachability(activeStack); | ||||
| 	auto dists = getReachabilityWithEnemyBypass(activeStack, damageCache, hb); | ||||
| 	auto flying = activeStack->hasBonusOfType(BonusType::FLYING); | ||||
|  | ||||
| 	for(const battle::Unit * enemy : targets.unreachableEnemies) | ||||
| 	{ | ||||
| 		std::vector<const battle::Unit *> adjacentStacks = getAdjacentUnits(enemy); | ||||
| 		auto closestStack = *vstd::minElementByFun(adjacentStacks, [&](const battle::Unit * u) -> int64_t | ||||
| 			{ | ||||
| 				return dists.distToNearestNeighbour(activeStack, u) * 100000 - activeStack->getTotalHealth(); | ||||
| 			}); | ||||
| 		logAi->trace( | ||||
| 			"Checking movement towards %d of %s", | ||||
| 			enemy->getCount(), | ||||
| 			enemy->creatureId().toCreature()->getNameSingularTranslated()); | ||||
|  | ||||
| 		auto distance = dists.distToNearestNeighbour(activeStack, closestStack); | ||||
| 		auto distance = dists.distToNearestNeighbour(activeStack, enemy); | ||||
|  | ||||
| 		if(distance >= GameConstants::BFIELD_SIZE) | ||||
| 			continue; | ||||
| @@ -315,30 +347,94 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( | ||||
| 			continue; | ||||
|  | ||||
| 		auto turnsToRich = (distance - 1) / speed + 1; | ||||
| 		auto hexes = closestStack->getSurroundingHexes(); | ||||
| 		auto enemySpeed = closestStack->getMovementRange(); | ||||
| 		auto hexes = enemy->getSurroundingHexes(); | ||||
| 		auto enemySpeed = enemy->getMovementRange(); | ||||
| 		auto speedRatio = speed / static_cast<float>(enemySpeed); | ||||
| 		auto multiplier = speedRatio > 1 ? 1 : speedRatio; | ||||
|  | ||||
| 		if(enemy->canShoot()) | ||||
| 			multiplier *= 1.5f; | ||||
|  | ||||
| 		for(auto hex : hexes) | ||||
| 		for(auto & hex : hexes) | ||||
| 		{ | ||||
| 			// FIXME: provide distance info for Jousting bonus | ||||
| 			auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack)); | ||||
| 			auto bai = BattleAttackInfo(activeStack, enemy, 0, cb->battleCanShoot(activeStack)); | ||||
| 			auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb); | ||||
|  | ||||
| 			attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure | ||||
|  | ||||
| 			auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb); | ||||
| 			auto scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(multiplier / turnsToRich), score.ourDamageReduce); | ||||
| 			auto scorePerTurn = BattleScore(score.enemyDamageReduce * multiplier / turnsToRich, score.ourDamageReduce); | ||||
|  | ||||
| #if BATTLE_TRACE_LEVEL >= 1 | ||||
| 			logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.scorePerTurn, scoreValue(scorePerTurn)); | ||||
| #endif | ||||
|  | ||||
| 			if(result.scorePerTurn < scoreValue(scorePerTurn)) | ||||
| 			{ | ||||
| 				result.scorePerTurn = scoreValue(scorePerTurn); | ||||
| 				result.score = scoreValue(score); | ||||
| 				result.positions = closestStack->getAttackableHexes(activeStack); | ||||
| 				result.positions.clear(); | ||||
|  | ||||
| #if BATTLE_TRACE_LEVEL >= 1 | ||||
| 				logAi->trace("New high score"); | ||||
| #endif | ||||
|  | ||||
| 				for(BattleHex enemyHex : enemy->getAttackableHexes(activeStack)) | ||||
| 				{ | ||||
| 					while(!flying && dists.distances[enemyHex] > speed) | ||||
| 					{ | ||||
| 						enemyHex = dists.predecessors.at(enemyHex); | ||||
| 						if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK) | ||||
| 						{ | ||||
| 							auto defenderToBypass = hb->battleGetUnitByPos(enemyHex); | ||||
|  | ||||
| 							if(defenderToBypass) | ||||
| 							{ | ||||
| #if BATTLE_TRACE_LEVEL >= 1 | ||||
| 								logAi->trace("Found target to bypass at %d", enemyHex.hex); | ||||
| #endif | ||||
|  | ||||
| 								auto attackHex = dists.predecessors[enemyHex]; | ||||
| 								auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack)); | ||||
| 								auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb); | ||||
|  | ||||
| 								auto adjacentStacks = getAdjacentUnits(enemy); | ||||
|  | ||||
| 								adjacentStacks.push_back(defenderToBypass); | ||||
| 								vstd::removeDuplicates(adjacentStacks); | ||||
|  | ||||
| 								auto bypassScore = calculateExchange( | ||||
| 									attackBypass, | ||||
| 									dists.distances[attackHex], | ||||
| 									targets, | ||||
| 									damageCache, | ||||
| 									hb, | ||||
| 									adjacentStacks); | ||||
|  | ||||
| 								if(scoreValue(bypassScore) > result.score) | ||||
| 								{ | ||||
| 									auto newMultiplier = multiplier * speed * turnsToRich / dists.distances[attackHex]; | ||||
|  | ||||
| 									result.score = scoreValue(bypassScore); | ||||
|  | ||||
| 									scorePerTurn = BattleScore( | ||||
| 										score.enemyDamageReduce  * newMultiplier, | ||||
| 										score.ourDamageReduce); | ||||
|  | ||||
| 									result.scorePerTurn = scoreValue(scorePerTurn); | ||||
|  | ||||
| #if BATTLE_TRACE_LEVEL >= 1 | ||||
| 									logAi->trace("New high score after bypass %f", scoreValue(scorePerTurn)); | ||||
| #endif | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					result.positions.push_back(enemyHex); | ||||
| 				} | ||||
|  | ||||
| 				result.cachedAttack = attack; | ||||
| 				result.turnsToRich = turnsToRich; | ||||
| 			} | ||||
| @@ -382,7 +478,8 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( | ||||
| 	const AttackPossibility & ap, | ||||
| 	uint8_t turn, | ||||
| 	PotentialTargets & targets, | ||||
| 	std::shared_ptr<HypotheticBattle> hb) const | ||||
| 	std::shared_ptr<HypotheticBattle> hb, | ||||
| 	std::vector<const battle::Unit *> additionalUnits) const | ||||
| { | ||||
| 	ReachabilityData result; | ||||
|  | ||||
| @@ -390,7 +487,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( | ||||
|  | ||||
| 	if(!ap.attack.shooting) hexes.push_back(ap.from); | ||||
|  | ||||
| 	std::vector<const battle::Unit *> allReachableUnits; | ||||
| 	std::vector<const battle::Unit *> allReachableUnits = additionalUnits; | ||||
|  | ||||
| 	for(auto hex : hexes) | ||||
| 	{ | ||||
| @@ -432,7 +529,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( | ||||
|  | ||||
| 	for(auto unit : allReachableUnits) | ||||
| 	{ | ||||
| 		auto accessible = !unit->canShoot(); | ||||
| 		auto accessible = !unit->canShoot() || vstd::contains(additionalUnits, unit); | ||||
|  | ||||
| 		if(!accessible) | ||||
| 		{ | ||||
| @@ -494,7 +591,8 @@ BattleScore BattleExchangeEvaluator::calculateExchange( | ||||
| 	uint8_t turn, | ||||
| 	PotentialTargets & targets, | ||||
| 	DamageCache & damageCache, | ||||
| 	std::shared_ptr<HypotheticBattle> hb) const | ||||
| 	std::shared_ptr<HypotheticBattle> hb, | ||||
| 	std::vector<const battle::Unit *> additionalUnits) const | ||||
| { | ||||
| #if BATTLE_TRACE_LEVEL>=1 | ||||
| 	logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex); | ||||
| @@ -513,7 +611,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange( | ||||
| 	if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) | ||||
| 		enemyStacks.push_back(ap.attack.defender); | ||||
|  | ||||
| 	ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb); | ||||
| 	ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb, additionalUnits); | ||||
|  | ||||
| 	if(exchangeUnits.units.empty()) | ||||
| 	{ | ||||
|   | ||||
| @@ -139,7 +139,8 @@ private: | ||||
| 		uint8_t turn, | ||||
| 		PotentialTargets & targets, | ||||
| 		DamageCache & damageCache, | ||||
| 		std::shared_ptr<HypotheticBattle> hb) const; | ||||
| 		std::shared_ptr<HypotheticBattle> hb, | ||||
| 		std::vector<const battle::Unit *> additionalUnits = {}) const; | ||||
|  | ||||
| 	bool canBeHitThisTurn(const AttackPossibility & ap); | ||||
|  | ||||
| @@ -171,7 +172,8 @@ public: | ||||
| 		const AttackPossibility & ap, | ||||
| 		uint8_t turn, | ||||
| 		PotentialTargets & targets, | ||||
| 		std::shared_ptr<HypotheticBattle> hb) const; | ||||
| 		std::shared_ptr<HypotheticBattle> hb, | ||||
| 		std::vector<const battle::Unit *> additionalUnits = {}) const; | ||||
|  | ||||
| 	bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position); | ||||
|  | ||||
|   | ||||
| @@ -18,9 +18,19 @@ VCMI_LIB_NAMESPACE_BEGIN | ||||
| bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, BattleSide side) const | ||||
| { | ||||
| 	//at(otherHex) != EAccessibility::ACCESSIBLE && (at(otherHex) != EAccessibility::GATE || side != BattleSide::DEFENDER) | ||||
| 	if(at(tile) != EAccessibility::ACCESSIBLE) | ||||
| 		if(at(tile) != EAccessibility::GATE || side != BattleSide::DEFENDER) | ||||
| 	auto accessibility = at(tile); | ||||
|  | ||||
| 	if(accessibility == EAccessibility::ALIVE_STACK) | ||||
| 	{ | ||||
| 		auto destructible = destructibleEnemyTurns.find(tile); | ||||
|  | ||||
| 		return destructible != destructibleEnemyTurns.end(); | ||||
| 	} | ||||
|  | ||||
| 	if(accessibility != EAccessibility::ACCESSIBLE) | ||||
| 		if(accessibility != EAccessibility::GATE || side != BattleSide::DEFENDER) | ||||
| 			return false; | ||||
|  | ||||
| 	return true; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -35,6 +35,8 @@ using TAccessibilityArray = std::array<EAccessibility, GameConstants::BFIELD_SIZ | ||||
|  | ||||
| struct DLL_LINKAGE AccessibilityInfo : TAccessibilityArray | ||||
| { | ||||
| 	std::map<BattleHex, ui8> destructibleEnemyTurns; | ||||
|  | ||||
| 	public: | ||||
| 		bool accessible(BattleHex tile, const battle::Unit * stack) const; //checks for both tiles if stack is double wide | ||||
| 		bool accessible(BattleHex tile, bool doubleWide, BattleSide side) const; //checks for both tiles if stack is double wide | ||||
|   | ||||
| @@ -1052,16 +1052,29 @@ ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibi | ||||
| 			continue; | ||||
|  | ||||
| 		const int costToNeighbour = ret.distances[curHex.hex] + 1; | ||||
|  | ||||
| 		for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex]) | ||||
| 		{ | ||||
| 			if(neighbour.isValid()) | ||||
| 			{ | ||||
| 				auto additionalCost = 0; | ||||
|  | ||||
| 				if(params.bypassEnemyStacks) | ||||
| 				{ | ||||
| 					auto enemyToBypass = params.destructibleEnemyTurns.find(neighbour); | ||||
|  | ||||
| 					if(enemyToBypass != params.destructibleEnemyTurns.end()) | ||||
| 					{ | ||||
| 						additionalCost = enemyToBypass->second; | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				const int costFoundSoFar = ret.distances[neighbour.hex]; | ||||
|  | ||||
| 				if(accessibleCache[neighbour.hex] && costToNeighbour < costFoundSoFar) | ||||
| 				if(accessibleCache[neighbour.hex] && costToNeighbour + additionalCost < costFoundSoFar) | ||||
| 				{ | ||||
| 					hexq.push(neighbour); | ||||
| 					ret.distances[neighbour.hex] = costToNeighbour; | ||||
| 					ret.distances[neighbour.hex] = costToNeighbour + additionalCost; | ||||
| 					ret.predecessors[neighbour.hex] = curHex; | ||||
| 				} | ||||
| 			} | ||||
| @@ -1236,7 +1249,13 @@ ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Pa | ||||
| 	if(params.flying) | ||||
| 		return getFlyingReachability(params); | ||||
| 	else | ||||
| 		return makeBFS(getAccessibility(params.knownAccessible), params); | ||||
| 	{ | ||||
| 		auto accessibility = getAccessibility(params.knownAccessible); | ||||
|  | ||||
| 		accessibility.destructibleEnemyTurns = params.destructibleEnemyTurns; | ||||
|  | ||||
| 		return makeBFS(accessibility, params); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters ¶ms) const | ||||
|   | ||||
| @@ -29,7 +29,9 @@ struct DLL_LINKAGE ReachabilityInfo | ||||
| 		bool doubleWide = false; | ||||
| 		bool flying = false; | ||||
| 		bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes | ||||
| 		bool bypassEnemyStacks = false; // in case of true will count amount of turns needed to kill enemy and thus move forward | ||||
| 		std::vector<BattleHex> knownAccessible; //hexes that will be treated as accessible, even if they're occupied by stack (by default - tiles occupied by stack we do reachability for, so it doesn't block itself) | ||||
| 		std::map<BattleHex, ui8> destructibleEnemyTurns; // hom many turns it is needed to kill enemy on specific hex | ||||
|  | ||||
| 		BattleHex startPosition; //assumed position of stack | ||||
| 		BattleSide perspective = BattleSide::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side | ||||
|   | ||||
		Reference in New Issue
	
	Block a user