1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-03-17 20:58:07 +02:00

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.
This commit is contained in:
Xilmi 2024-12-05 21:09:24 +01:00
parent 3f073507a1
commit df21a77857
4 changed files with 128 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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