mirror of
https://github.com/vcmi/vcmi.git
synced 2025-07-17 01:32:21 +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:
@ -119,6 +119,58 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
|
|||||||
return result;
|
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)
|
std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
|
||||||
{
|
{
|
||||||
//TODO: faerie dragon type spell should be selected by server
|
//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);
|
auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
|
||||||
float score = EvaluationResult::INEFFECTIVE_SCORE;
|
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())
|
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());
|
logAi->trace("Evaluating attack for %s", stack->getDescription());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
|
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense);
|
||||||
auto & bestAttack = evaluationResult.bestAttack;
|
auto & bestAttack = evaluationResult.bestAttack;
|
||||||
|
|
||||||
cachedAttack.ap = bestAttack;
|
cachedAttack.ap = bestAttack;
|
||||||
@ -227,15 +292,13 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
|
|||||||
return BattleAction::makeDefend(stack);
|
return BattleAction::makeDefend(stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool
|
bool isTargetOutsideFort = std::none_of(castleHexes.begin(), castleHexes.end(),
|
||||||
{
|
[&](const BattleHex& hex) {
|
||||||
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
|
return hex == bestAttack.from;
|
||||||
});
|
});
|
||||||
|
|
||||||
bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4;
|
|
||||||
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
|
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
|
||||||
&& !bestAttack.attack.shooting
|
&& !bestAttack.attack.shooting
|
||||||
&& hb->battleGetFortifications().hasMoat
|
&& hasWorkingTowers()
|
||||||
&& !enemyMellee.empty()
|
&& !enemyMellee.empty()
|
||||||
&& isTargetOutsideFort;
|
&& isTargetOutsideFort;
|
||||||
|
|
||||||
@ -349,6 +412,28 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
|
|||||||
auto reachability = cb->getBattle(battleID)->getReachability(stack);
|
auto reachability = cb->getBattle(battleID)->getReachability(stack);
|
||||||
auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
|
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
|
if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
|
||||||
{
|
{
|
||||||
return BattleAction::makeDefend(stack);
|
return BattleAction::makeDefend(stack);
|
||||||
|
@ -53,6 +53,8 @@ public:
|
|||||||
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
|
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
|
||||||
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
|
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
|
||||||
std::vector<BattleHex> getBrokenWallMoatHexes() const;
|
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 evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
|
||||||
void print(const std::string & text) const;
|
void print(const std::string & text) const;
|
||||||
BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);
|
BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
#include "StdInc.h"
|
#include "StdInc.h"
|
||||||
#include "BattleExchangeVariant.h"
|
#include "BattleExchangeVariant.h"
|
||||||
|
#include "BattleEvaluator.h"
|
||||||
#include "../../lib/CStack.h"
|
#include "../../lib/CStack.h"
|
||||||
|
|
||||||
AttackerValue::AttackerValue()
|
AttackerValue::AttackerValue()
|
||||||
@ -213,9 +214,11 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
|
|||||||
const battle::Unit * activeStack,
|
const battle::Unit * activeStack,
|
||||||
PotentialTargets & targets,
|
PotentialTargets & targets,
|
||||||
DamageCache & damageCache,
|
DamageCache & damageCache,
|
||||||
std::shared_ptr<HypotheticBattle> hb)
|
std::shared_ptr<HypotheticBattle> hb,
|
||||||
|
bool siegeDefense)
|
||||||
{
|
{
|
||||||
EvaluationResult result(targets.bestAction());
|
EvaluationResult result(targets.bestAction());
|
||||||
|
std::vector<BattleHex> castleHexes = BattleEvaluator::getCastleHexes();
|
||||||
|
|
||||||
if(!activeStack->waited() && !activeStack->acquireState()->hadMorale)
|
if(!activeStack->waited() && !activeStack->acquireState()->hadMorale)
|
||||||
{
|
{
|
||||||
@ -231,6 +234,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
|
|||||||
|
|
||||||
for(auto & ap : targets.possibleAttacks)
|
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);
|
float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
|
||||||
|
|
||||||
if(score > result.score)
|
if(score > result.score)
|
||||||
@ -263,6 +269,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
|
|||||||
|
|
||||||
for(auto & ap : targets.possibleAttacks)
|
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);
|
float score = evaluateExchange(ap, 0, targets, damageCache, hb);
|
||||||
bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
|
bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
|
||||||
|
|
||||||
@ -350,11 +359,32 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
|
|||||||
if(distance <= speed)
|
if(distance <= speed)
|
||||||
continue;
|
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 turnsToRich = (distance - 1) / speed + 1;
|
||||||
auto hexes = enemy->getSurroundingHexes();
|
auto hexes = enemy->getSurroundingHexes();
|
||||||
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;
|
auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
|
||||||
|
|
||||||
for(auto & hex : hexes)
|
for(auto & hex : hexes)
|
||||||
{
|
{
|
||||||
|
@ -159,7 +159,8 @@ public:
|
|||||||
const battle::Unit * activeStack,
|
const battle::Unit * activeStack,
|
||||||
PotentialTargets & targets,
|
PotentialTargets & targets,
|
||||||
DamageCache & damageCache,
|
DamageCache & damageCache,
|
||||||
std::shared_ptr<HypotheticBattle> hb);
|
std::shared_ptr<HypotheticBattle> hb,
|
||||||
|
bool siegeDefense = false);
|
||||||
|
|
||||||
float evaluateExchange(
|
float evaluateExchange(
|
||||||
const AttackPossibility & ap,
|
const AttackPossibility & ap,
|
||||||
|
Reference in New Issue
Block a user