1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-01-26 03:52:01 +02:00

BattleAI: improve movement towards unreachable

This commit is contained in:
Andrii Danylchenko 2024-08-17 18:17:46 +03:00
parent 26609d7a4f
commit bc80532f29
5 changed files with 87 additions and 32 deletions

View File

@ -201,6 +201,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
if(attackInfo.shooting) if(attackInfo.shooting)
return 0; return 0;
std::set<uint32_t> checkedUnits;
auto attacker = attackInfo.attacker; auto attacker = attackInfo.attacker;
auto hexes = attacker->getSurroundingHexes(hex); auto hexes = attacker->getSurroundingHexes(hex);
for(BattleHex tile : hexes) for(BattleHex tile : hexes)
@ -208,9 +210,13 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto st = state->battleGetUnitByPos(tile, true); auto st = state->battleGetUnitByPos(tile, true);
if(!st || !state->battleMatchOwner(st, attacker)) if(!st || !state->battleMatchOwner(st, attacker))
continue; continue;
if(vstd::contains(checkedUnits, st->unitId()))
continue;
if(!state->battleCanShoot(st)) if(!state->battleCanShoot(st))
continue; continue;
checkedUnits.insert(st->unitId());
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
BattleAttackInfo rangeAttackInfo(st, attacker, 0, true); BattleAttackInfo rangeAttackInfo(st, attacker, 0, true);
rangeAttackInfo.defenderPos = hex; rangeAttackInfo.defenderPos = hex;
@ -220,9 +226,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo); auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo); auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo);
auto cachedDmg = damageCache.getOriginalDamage(st, attacker, state);
int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1;
res += gain; res += gain * cachedDmg / std::max<uint64_t>(1, averageDmg(rangeDmg.damage));
} }
return res; return res;

View File

@ -167,7 +167,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
score score
); );
if (moveTarget.scorePerTurn <= score) if (moveTarget.score <= score)
{ {
if(evaluationResult.wait) if(evaluationResult.wait)
{ {
@ -197,7 +197,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
} }
//ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code. //ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code.
if(moveTarget.scorePerTurn > score) if(moveTarget.score > score)
{ {
score = moveTarget.score; score = moveTarget.score;
cachedAttack = moveTarget.cachedAttack; cachedAttack = moveTarget.cachedAttack;
@ -206,12 +206,11 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(stack->waited()) if(stack->waited())
{ {
logAi->debug( logAi->debug(
"Moving %s towards hex %s[%d], score: %2f/%2f", "Moving %s towards hex %s[%d], score: %2f",
stack->getDescription(), stack->getDescription(),
moveTarget.cachedAttack->attack.defender->getDescription(), moveTarget.cachedAttack->attack.defender->getDescription(),
moveTarget.cachedAttack->attack.defender->getPosition().hex, moveTarget.cachedAttack->attack.defender->getPosition().hex,
moveTarget.score, moveTarget.score);
moveTarget.scorePerTurn);
return goTowardsNearest(stack, moveTarget.positions, *targets); return goTowardsNearest(stack, moveTarget.positions, *targets);
} }

View File

@ -18,7 +18,7 @@ AttackerValue::AttackerValue()
} }
MoveTarget::MoveTarget() MoveTarget::MoveTarget()
: positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE), scorePerTurn(EvaluationResult::INEFFECTIVE_SCORE) : positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE)
{ {
turnsToRich = 1; turnsToRich = 1;
} }
@ -292,7 +292,7 @@ ReachabilityInfo getReachabilityWithEnemyBypass(
continue; continue;
auto dmg = damageCache.getOriginalDamage(activeStack, unit, state); auto dmg = damageCache.getOriginalDamage(activeStack, unit, state);
auto turnsToKill = unit->getAvailableHealth() / dmg + 1; auto turnsToKill = unit->getAvailableHealth() / dmg;
vstd::amin(turnsToKill, 100); vstd::amin(turnsToKill, 100);
@ -364,15 +364,16 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure 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 score = calculateExchange(attack, turnsToRich, targets, damageCache, hb);
auto scorePerTurn = BattleScore(score.enemyDamageReduce * multiplier / turnsToRich, score.ourDamageReduce);
score.enemyDamageReduce *= multiplier;
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.scorePerTurn, scoreValue(scorePerTurn)); logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.score, scoreValue(score));
#endif #endif
if(result.scorePerTurn < scoreValue(scorePerTurn)) if(result.score < scoreValue(score)
|| (result.turnsToRich > turnsToRich && vstd::isAlmostEqual(result.score, scoreValue(score))))
{ {
result.scorePerTurn = scoreValue(scorePerTurn);
result.score = scoreValue(score); result.score = scoreValue(score);
result.positions.clear(); result.positions.clear();
@ -414,18 +415,10 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
if(scoreValue(bypassScore) > result.score) if(scoreValue(bypassScore) > result.score)
{ {
auto newMultiplier = multiplier * speed * turnsToRich / dists.distances[attackHex];
result.score = scoreValue(bypassScore); result.score = scoreValue(bypassScore);
scorePerTurn = BattleScore(
score.enemyDamageReduce * newMultiplier,
score.ourDamageReduce);
result.scorePerTurn = scoreValue(scorePerTurn);
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace("New high score after bypass %f", scoreValue(scorePerTurn)); logAi->trace("New high score after bypass %f", scoreValue(bypassScore));
#endif #endif
} }
} }
@ -488,12 +481,25 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
if(!ap.attack.shooting) hexes.push_back(ap.from); if(!ap.attack.shooting) hexes.push_back(ap.from);
std::vector<const battle::Unit *> allReachableUnits = additionalUnits; std::vector<const battle::Unit *> allReachableUnits = additionalUnits;
for(auto hex : hexes) for(auto hex : hexes)
{ {
vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex)); vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex));
} }
for(auto hex : ap.attack.attacker->getHexes())
{
auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex);
for(auto unit : unitsReachingAttacker)
{
if(unit->unitSide() != ap.attack.attacker->unitSide())
{
allReachableUnits.push_back(unit);
result.enemyUnitsReachingAttacker.insert(unit->unitId());
}
}
}
vstd::removeDuplicates(allReachableUnits); vstd::removeDuplicates(allReachableUnits);
auto copy = allReachableUnits; auto copy = allReachableUnits;
@ -621,9 +627,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb); auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb);
BattleExchangeVariant v; BattleExchangeVariant v;
for(int turn = 0; turn < exchangeUnits.units.size(); turn++) for(int exchangeTurn = 0; exchangeTurn < exchangeUnits.units.size(); exchangeTurn++)
{ {
for(auto unit : exchangeUnits.units.at(turn)) for(auto unit : exchangeUnits.units.at(exchangeTurn))
{ {
if(unit->isTurret()) if(unit->isTurret())
continue; continue;
@ -652,16 +658,26 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
}); });
bool canUseAp = true; bool canUseAp = true;
const int totalTurnsCount = 10;
for(int turn = 0; turn < exchangeUnits.units.size(); turn++) std::set<uint32_t> blockedShooters;
for(int exchangeTurn = 0; exchangeTurn < totalTurnsCount; exchangeTurn++)
{ {
for(auto activeUnit : exchangeUnits.units.at(turn)) bool isMovingTurm = exchangeTurn < turn;
int queueTurn = exchangeTurn >= exchangeUnits.units.size()
? exchangeUnits.units.size() - 1
: exchangeTurn;
for(auto activeUnit : exchangeUnits.units.at(queueTurn))
{ {
bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true); bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true);
battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks; battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks;
battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks; battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId()); auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get())
&& !vstd::contains(blockedShooters, attacker->unitId());
if(!attacker->alive()) if(!attacker->alive())
{ {
@ -672,10 +688,23 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
continue; continue;
} }
if(isMovingTurm && !shooting
&& !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId()))
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Attacker is moving");
#endif
continue;
}
auto targetUnit = ap.attack.defender; auto targetUnit = ap.attack.defender;
if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive()) if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive())
{ {
#if BATTLE_TRACE_LEVEL>=2
logAi->trace("Best target selector for %s", attacker->getDescription());
#endif
auto estimateAttack = [&](const battle::Unit * u) -> float auto estimateAttack = [&](const battle::Unit * u) -> float
{ {
auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId()); auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId());
@ -688,7 +717,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
hb, hb,
true); true);
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=2
logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score); logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score);
#endif #endif
@ -741,7 +770,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
} }
auto defender = exchangeBattle->getForUpdate(targetUnit->unitId()); auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get());
const int totalAttacks = attacker->getTotalAttacks(shooting); const int totalAttacks = attacker->getTotalAttacks(shooting);
if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
@ -760,6 +788,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
} }
} }
if(!shooting)
blockedShooters.insert(defender->unitId());
canUseAp = false; canUseAp = false;
vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool
@ -785,11 +816,20 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
for(auto hex : hexes) for(auto hex : hexes)
reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex);
auto score = v.getScore();
if(turn > 0)
{
auto turnMultiplier = 1 - std::min(0.2, 0.05 * turn);
score.enemyDamageReduce *= turnMultiplier;
}
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce); logAi->trace("Exchange score: enemy: %2f, our -%2f", score.enemyDamageReduce, score.ourDamageReduce);
#endif #endif
return v.getScore(); return score;
} }
bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)

View File

@ -54,7 +54,6 @@ struct AttackerValue
struct MoveTarget struct MoveTarget
{ {
float score; float score;
float scorePerTurn;
std::vector<BattleHex> positions; std::vector<BattleHex> positions;
std::optional<AttackPossibility> cachedAttack; std::optional<AttackPossibility> cachedAttack;
uint8_t turnsToRich; uint8_t turnsToRich;
@ -64,7 +63,7 @@ struct MoveTarget
struct EvaluationResult struct EvaluationResult
{ {
static const int64_t INEFFECTIVE_SCORE = -10000; static const int64_t INEFFECTIVE_SCORE = -100000000;
AttackPossibility bestAttack; AttackPossibility bestAttack;
MoveTarget bestMove; MoveTarget bestMove;
@ -120,6 +119,8 @@ struct ReachabilityData
// far shooters // far shooters
std::vector<const battle::Unit *> shooters; std::vector<const battle::Unit *> shooters;
std::set<uint32_t> enemyUnitsReachingAttacker;
}; };
class BattleExchangeEvaluator class BattleExchangeEvaluator

View File

@ -89,6 +89,14 @@ void DangerHitMapAnalyzer::updateHitMap()
heroes[hero->tempOwner][hero] = HeroRole::MAIN; heroes[hero->tempOwner][hero] = HeroRole::MAIN;
} }
if(obj->ID == Obj::TOWN)
{
auto town = dynamic_cast<const CGTownInstance *>(obj);
if(town->garrisonHero)
heroes[town->garrisonHero->tempOwner][town->garrisonHero] = HeroRole::MAIN;
}
} }
auto ourTowns = cb->getTownsInfo(); auto ourTowns = cb->getTownsInfo();