ソースを参照

Battle-AI-improvements

When defending the AI is now much smarter to use their defensive-structures like walls, towers and the moat to their advantage instead of allowing them to be lured out and killed in the open.

A penalty-multiplier is now applied when deciding which units to walk towards. If an ally is closer than us to the enemy unit in question, we reduce our score for walking towards that unit too. This shall help against baiting a whole flock of AI-stacks to overcommit on chasing an inferior stack of the enemy.
Xilmi 10 ヶ月 前
コミット
df21a77857

+ 92 - 7
AI/BattleAI/BattleEvaluator.cpp

@@ -119,6 +119,58 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 	return result;
 }
 
+std::vector<BattleHex> BattleEvaluator::getCastleHexes()
+{
+	std::vector<BattleHex> result;
+
+	// Loop through all wall parts
+
+	std::vector<BattleHex> wallHexes;
+	wallHexes.push_back(50);
+	wallHexes.push_back(183);
+	wallHexes.push_back(182);
+	wallHexes.push_back(130);
+	wallHexes.push_back(78);
+	wallHexes.push_back(29);
+	wallHexes.push_back(12);
+	wallHexes.push_back(97);
+	wallHexes.push_back(45);
+	wallHexes.push_back(62);
+	wallHexes.push_back(112);
+	wallHexes.push_back(147);
+	wallHexes.push_back(165);
+
+	for (BattleHex wallHex : wallHexes) {
+		// Get the starting x-coordinate of the wall hex
+		int startX = wallHex.getX();
+
+		// Initialize current hex with the wall hex
+		BattleHex currentHex = wallHex;
+		while (currentHex.isValid()) {
+			// Check if the x-coordinate has wrapped (smaller than the starting x)
+			if (currentHex.getX() < startX) {
+				break;
+			}
+
+			// Add the hex to the result
+			result.push_back(currentHex);
+
+			// Move to the next hex to the right
+			currentHex = currentHex.cloneInDirection(BattleHex::RIGHT, false);
+		}
+	}
+
+	return result;
+}
+
+bool BattleEvaluator::hasWorkingTowers() const
+{
+	bool keepIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
+	bool upperIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
+	bool bottomIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
+	return keepIntact || upperIntact || bottomIntact;
+}
+
 std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
 {
 	//TODO: faerie dragon type spell should be selected by server
@@ -161,6 +213,19 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 
 	auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
 	float score = EvaluationResult::INEFFECTIVE_SCORE;
+	auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
+		{
+			return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
+		});
+	bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
+		&& !stack->canShoot()
+		&& hasWorkingTowers()
+		&& !enemyMellee.empty();
+	std::vector<BattleHex> castleHexes = getCastleHexes();
+	for (auto hex : castleHexes)
+	{
+		logAi->trace("Castlehex ID: %d Y: %d X: %d", hex, hex.getY(), hex.getX());
+	}
 
 	if(targets->possibleAttacks.empty() && bestSpellcast.has_value())
 	{
@@ -174,7 +239,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		logAi->trace("Evaluating attack for %s", stack->getDescription());
 #endif
 
-		auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
+		auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense);
 		auto & bestAttack = evaluationResult.bestAttack;
 
 		cachedAttack.ap = bestAttack;
@@ -227,15 +292,13 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 						return BattleAction::makeDefend(stack);
 					}
 
-					auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool
-						{
-							return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
+					bool isTargetOutsideFort = std::none_of(castleHexes.begin(), castleHexes.end(),
+						[&](const BattleHex& hex) {
+							return hex == bestAttack.from;
 						});
-
-					bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4;
 					bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
 						&& !bestAttack.attack.shooting
-						&& hb->battleGetFortifications().hasMoat
+						&& hasWorkingTowers()
 						&& !enemyMellee.empty()
 						&& isTargetOutsideFort;
 
@@ -349,6 +412,28 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 	auto reachability = cb->getBattle(battleID)->getReachability(stack);
 	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
 
+	auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
+		{
+			return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
+		});
+
+	bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
+		&& hasWorkingTowers()
+		&& !enemyMellee.empty();
+
+	if (siegeDefense)
+	{
+		vstd::erase_if(avHexes, [&](const BattleHex& hex) {
+			std::vector<BattleHex> castleHexes = getCastleHexes();
+
+			bool isOutsideWall = std::none_of(castleHexes.begin(), castleHexes.end(),
+				[&](const BattleHex& checkhex) {
+					return checkhex == hex;
+				});
+			return isOutsideWall;
+			});
+	}
+
 	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
 	{
 		return BattleAction::makeDefend(stack);

+ 2 - 0
AI/BattleAI/BattleEvaluator.h

@@ -53,6 +53,8 @@ public:
 	std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
 	BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
 	std::vector<BattleHex> getBrokenWallMoatHexes() const;
+	static std::vector<BattleHex> getCastleHexes();
+	bool hasWorkingTowers() const;
 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
 	void print(const std::string & text) const;
 	BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);

+ 32 - 2
AI/BattleAI/BattleExchangeVariant.cpp

@@ -9,6 +9,7 @@
  */
 #include "StdInc.h"
 #include "BattleExchangeVariant.h"
+#include "BattleEvaluator.h"
 #include "../../lib/CStack.h"
 
 AttackerValue::AttackerValue()
@@ -213,9 +214,11 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 	const battle::Unit * activeStack,
 	PotentialTargets & targets,
 	DamageCache & damageCache,
-	std::shared_ptr<HypotheticBattle> hb)
+	std::shared_ptr<HypotheticBattle> hb,
+	bool siegeDefense)
 {
 	EvaluationResult result(targets.bestAction());
+	std::vector<BattleHex> castleHexes = BattleEvaluator::getCastleHexes();
 
 	if(!activeStack->waited() && !activeStack->acquireState()->hadMorale)
 	{
@@ -231,6 +234,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 		for(auto & ap : targets.possibleAttacks)
 		{
+			if (siegeDefense && std::find(castleHexes.begin(), castleHexes.end(), ap.from) == castleHexes.end())
+				continue;
+
 			float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
 
 			if(score > result.score)
@@ -263,6 +269,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 
 	for(auto & ap : targets.possibleAttacks)
 	{
+		if (siegeDefense && std::find(castleHexes.begin(), castleHexes.end(), ap.from) == castleHexes.end())
+			continue;
+
 		float score = evaluateExchange(ap, 0, targets, damageCache, hb);
 		bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
 
@@ -350,11 +359,32 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 		if(distance <= speed)
 			continue;
 
+		float penaltyMultiplier = 1.0f; // Default multiplier, no penalty
+		float closestAllyDistance = std::numeric_limits<float>::max();
+
+		for (const battle::Unit* ally : hb->battleAliveUnits()) {
+			if (ally == activeStack) 
+				continue;
+			if (ally->unitSide() != activeStack->unitSide()) 
+				continue;
+
+			float allyDistance = dists.distToNearestNeighbour(ally, enemy);
+			if (allyDistance < closestAllyDistance)
+			{
+				closestAllyDistance = allyDistance;
+			}
+		}
+
+		// If an ally is closer to the enemy, compute the penaltyMultiplier
+		if (closestAllyDistance < distance) {
+			penaltyMultiplier = closestAllyDistance / distance; // Ratio of distances
+		}
+
 		auto turnsToRich = (distance - 1) / speed + 1;
 		auto hexes = enemy->getSurroundingHexes();
 		auto enemySpeed = enemy->getMovementRange();
 		auto speedRatio = speed / static_cast<float>(enemySpeed);
-		auto multiplier = speedRatio > 1 ? 1 : speedRatio;
+		auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
 
 		for(auto & hex : hexes)
 		{

+ 2 - 1
AI/BattleAI/BattleExchangeVariant.h

@@ -159,7 +159,8 @@ public:
 		const battle::Unit * activeStack,
 		PotentialTargets & targets,
 		DamageCache & damageCache,
-		std::shared_ptr<HypotheticBattle> hb);
+		std::shared_ptr<HypotheticBattle> hb,
+		bool siegeDefense = false);
 
 	float evaluateExchange(
 		const AttackPossibility & ap,