Kaynağa Gözat

BattleAI: fix bait for archers when need to go long way

Andrii Danylchenko 1 yıl önce
ebeveyn
işleme
8cdfa26fb5

+ 24 - 4
AI/BattleAI/BattleEvaluator.cpp

@@ -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))

+ 1 - 1
AI/BattleAI/BattleEvaluator.h

@@ -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;

+ 116 - 18
AI/BattleAI/BattleExchangeVariant.cpp

@@ -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())
 	{

+ 4 - 2
AI/BattleAI/BattleExchangeVariant.h

@@ -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);
 

+ 12 - 2
lib/battle/AccessibilityInfo.cpp

@@ -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;
 }
 

+ 2 - 0
lib/battle/AccessibilityInfo.h

@@ -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

+ 22 - 3
lib/battle/CBattleInfoCallback.cpp

@@ -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 &params) const

+ 2 - 0
lib/battle/ReachabilityInfo.h

@@ -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