1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-07-13 01:20:34 +02:00

Merge pull request #5739 from IvanSavenko/battleai_fix

Fix handling of double-wide creatures by BattleAI
This commit is contained in:
Ivan Savenko
2025-05-25 11:26:01 +03:00
committed by GitHub
5 changed files with 122 additions and 26 deletions

View File

@ -385,7 +385,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
} }
auto turnsToReach = (distance - 1) / speed + 1; auto turnsToReach = (distance - 1) / speed + 1;
const BattleHexArray & hexes = enemy->getSurroundingHexes(); const BattleHexArray & hexes = enemy->getAttackableHexes(activeStack);
auto enemySpeed = enemy->getMovementRange(); auto enemySpeed = enemy->getMovementRange();
auto speedRatio = speed / static_cast<float>(enemySpeed); auto speedRatio = speed / static_cast<float>(enemySpeed);
auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier; auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
@ -416,8 +416,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
logAi->trace("New high score"); logAi->trace("New high score");
#endif #endif
for(BattleHex enemyHex : enemy->getAttackableHexes(activeStack)) BattleHex enemyHex = hex;
{
while(!flying && dists.distances[enemyHex.toInt()] > speed && dists.predecessors.at(enemyHex.toInt()).isValid()) while(!flying && dists.distances[enemyHex.toInt()] > speed && dists.predecessors.at(enemyHex.toInt()).isValid())
{ {
enemyHex = dists.predecessors.at(enemyHex.toInt()); enemyHex = dists.predecessors.at(enemyHex.toInt());
@ -425,14 +424,17 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
if(dists.accessibility[enemyHex.toInt()] == EAccessibility::ALIVE_STACK) if(dists.accessibility[enemyHex.toInt()] == EAccessibility::ALIVE_STACK)
{ {
auto defenderToBypass = hb->battleGetUnitByPos(enemyHex); auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);
assert(defenderToBypass != nullptr);
auto attackHex = dists.predecessors[enemyHex.toInt()];
if(defenderToBypass) if(defenderToBypass &&
defenderToBypass != enemy &&
vstd::contains(defenderToBypass->getAttackableHexes(activeStack), attackHex))
{ {
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Found target to bypass at %d", enemyHex.toInt()); logAi->trace("Found target to bypass at %d", enemyHex.toInt());
#endif #endif
auto attackHex = dists.predecessors[enemyHex.toInt()];
auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack)); auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack));
auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb); auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb);
@ -462,8 +464,6 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
} }
result.positions.insert(enemyHex); result.positions.insert(enemyHex);
}
result.cachedAttack = attack; result.cachedAttack = attack;
result.turnsToReach = turnsToReach; result.turnsToReach = turnsToReach;
} }

View File

@ -1338,7 +1338,7 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(
attackOriginHex = attacker->occupiedHex(attackOriginHex); attackOriginHex = attacker->occupiedHex(attackOriginHex);
if (!vstd::contains(defender->getSurroundingHexes(defenderPos), attackOriginHex)) if (!vstd::contains(defender->getSurroundingHexes(defenderPos), attackOriginHex))
throw std::runtime_error("!!!"); throw std::runtime_error("Atempt to attack from invalid position!");
auto attackDirection = BattleHex::mutualPosition(attackOriginHex, defenderPos); auto attackDirection = BattleHex::mutualPosition(attackOriginHex, defenderPos);

View File

@ -68,22 +68,28 @@ const BattleHexArray & Unit::getSurroundingHexes(const BattleHex & position, boo
BattleHexArray Unit::getAttackableHexes(const Unit * attacker) const BattleHexArray Unit::getAttackableHexes(const Unit * attacker) const
{ {
const BattleHexArray & defenderHexes = getHexes(); if (!attacker->doubleWide())
BattleHexArray targetableHexes;
for(const auto & defenderHex : defenderHexes)
{ {
auto hexes = battle::Unit::getHexes(defenderHex); return getSurroundingHexes();
if(hexes.size() == 2 && BattleHex::getDistance(hexes.front(), hexes.back()) != 1)
hexes.pop_back();
for(const auto & hex : hexes)
targetableHexes.insert(hex.getNeighbouringTiles());
} }
else
{
BattleHexArray result;
return targetableHexes; for (const auto & attackOrigin : getSurroundingHexes())
{
if (!coversPos(attacker->occupiedHex(attackOrigin)) && attackOrigin.isAvailable())
result.insert(attackOrigin);
bool isAttacker = attacker->unitSide() == BattleSide::ATTACKER;
BattleHex::EDir headDirection = isAttacker ? BattleHex::RIGHT : BattleHex::LEFT;
BattleHex headHex = attackOrigin.cloneInDirection(headDirection);
if (!coversPos(headHex) && headHex.isAvailable())
result.insert(headHex);
}
return result;
}
} }
bool Unit::coversPos(const BattleHex & pos) const bool Unit::coversPos(const BattleHex & pos) const

View File

@ -132,6 +132,8 @@ public:
virtual std::string getDescription() const; virtual std::string getDescription() const;
const BattleHexArray & getSurroundingHexes(const BattleHex & assumedPosition = BattleHex::INVALID) const; // get six or 8 surrounding hexes depending on creature size const BattleHexArray & getSurroundingHexes(const BattleHex & assumedPosition = BattleHex::INVALID) const; // get six or 8 surrounding hexes depending on creature size
/// Returns list of hexes from which attacker can attack this unit
BattleHexArray getAttackableHexes(const Unit * attacker) const; BattleHexArray getAttackableHexes(const Unit * attacker) const;
static const BattleHexArray & getSurroundingHexes(const BattleHex & position, bool twoHex, BattleSide side); static const BattleHexArray & getSurroundingHexes(const BattleHex & position, bool twoHex, BattleSide side);

View File

@ -262,6 +262,96 @@ public:
} }
}; };
///// getAttackableHexes tests
TEST_F(AttackableHexesTest, getAttackableHexes_SingleWideAttacker_SingleWideDefender)
{
UnitFake & attacker = addRegularMelee(60, BattleSide::ATTACKER);
UnitFake & defender = addRegularMelee(90, BattleSide::DEFENDER);
static const BattleHexArray expectedDef =
{
72,
73,
89,
91,
106,
107
};
auto attackable = defender.getAttackableHexes(&attacker);
attackable.sort([](const auto & l, const auto & r) { return l < r; });
EXPECT_EQ(expectedDef, attackable);
}
TEST_F(AttackableHexesTest, getAttackableHexes_SingleWideAttacker_DoubleWideDefender)
{
UnitFake & attacker = addRegularMelee(60, BattleSide::ATTACKER);
UnitFake & defender = addDragon(90, BattleSide::DEFENDER);
static const BattleHexArray expectedDef =
{
72,
73,
74,
89,
92,
106,
107,
108
};
auto attackable = defender.getAttackableHexes(&attacker);
attackable.sort([](const auto & l, const auto & r) { return l < r; });
EXPECT_EQ(expectedDef, attackable);
}
TEST_F(AttackableHexesTest, getAttackableHexes_DoubleWideAttacker_SingleWideDefender)
{
UnitFake & attacker = addDragon(60, BattleSide::ATTACKER);
UnitFake & defender = addRegularMelee(90, BattleSide::DEFENDER);
static const BattleHexArray expectedDef =
{
72,
73,
74,
89,
92,
106,
107,
108
};
auto attackable = defender.getAttackableHexes(&attacker);
attackable.sort([](const auto & l, const auto & r) { return l < r; });
EXPECT_EQ(expectedDef, attackable);
}
TEST_F(AttackableHexesTest, getAttackableHexes_DoubleWideAttacker_DoubleWideDefender)
{
UnitFake & attacker = addDragon(60, BattleSide::ATTACKER);
UnitFake & defender = addDragon(90, BattleSide::DEFENDER);
static const BattleHexArray expectedDef =
{
72,
73,
74,
75,
89,
93,
106,
107,
108,
109
};
auto attackable = defender.getAttackableHexes(&attacker);
attackable.sort([](const auto & l, const auto & r) { return l < r; });
EXPECT_EQ(expectedDef, attackable);
}
//// CERBERI 3-HEADED ATTACKS //// CERBERI 3-HEADED ATTACKS
TEST_F(AttackableHexesTest, CerberiAttackerRight) TEST_F(AttackableHexesTest, CerberiAttackerRight)
@ -276,7 +366,6 @@ TEST_F(AttackableHexesTest, CerberiAttackerRight)
auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
EXPECT_TRUE(vstd::contains(attacked, &defender));
EXPECT_TRUE(vstd::contains(attacked, &right)); EXPECT_TRUE(vstd::contains(attacked, &right));
EXPECT_TRUE(vstd::contains(attacked, &left)); EXPECT_TRUE(vstd::contains(attacked, &left));
} }
@ -356,7 +445,6 @@ TEST_F(AttackableHexesTest, DragonRightRegular_RightHorithontalBreath)
auto attacked = getAttackedUnits(attacker, defender, defender.getPosition()); auto attacked = getAttackedUnits(attacker, defender, defender.getPosition());
EXPECT_TRUE(vstd::contains(attacked, &defender));
EXPECT_TRUE(vstd::contains(attacked, &next)); EXPECT_TRUE(vstd::contains(attacked, &next));
} }