From d55996cc46be2a6730b8552bf541c4979fd4fe3b Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 25 Aug 2024 15:49:11 +0300 Subject: [PATCH 1/2] Battle AI: fix firewall, fix haste spellcast evaluation for waits and movements, allow location spells --- AI/BattleAI/BattleEvaluator.cpp | 58 +++++++++++++++------------ AI/BattleAI/BattleEvaluator.h | 11 ++++- AI/BattleAI/BattleExchangeVariant.cpp | 4 +- AI/BattleAI/StackWithBonuses.cpp | 10 ++++- AI/BattleAI/StackWithBonuses.h | 2 + 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 74405c740..f45427e97 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -66,7 +66,6 @@ BattleEvaluator::BattleEvaluator( damageCache.buildDamageCache(hb, side); targets = std::make_unique(activeStack, damageCache, hb); - cachedScore = EvaluationResult::INEFFECTIVE_SCORE; } BattleEvaluator::BattleEvaluator( @@ -85,7 +84,6 @@ BattleEvaluator::BattleEvaluator( damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount) { targets = std::make_unique(activeStack, damageCache, hb); - cachedScore = EvaluationResult::INEFFECTIVE_SCORE; } std::vector BattleEvaluator::getBrokenWallMoatHexes() const @@ -178,8 +176,10 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb); auto & bestAttack = evaluationResult.bestAttack; - cachedAttack = bestAttack; - cachedScore = evaluationResult.score; + cachedAttack.ap = bestAttack; + cachedAttack.score = evaluationResult.score; + cachedAttack.turn = 0; + cachedAttack.waited = evaluationResult.wait; //TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc. if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff()) @@ -239,8 +239,9 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) if(moveTarget.score > score) { score = moveTarget.score; - cachedAttack = moveTarget.cachedAttack; - cachedScore = score; + cachedAttack.ap = moveTarget.cachedAttack; + cachedAttack.score = score; + cachedAttack.turn = moveTarget.turnsToRich; if(stack->waited()) { @@ -255,6 +256,8 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) } else { + cachedAttack.waited = true; + return BattleAction::makeWait(stack); } } @@ -448,7 +451,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) vstd::erase_if(possibleSpells, [](const CSpell *s) { - return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION; + return spellType(s) != SpellTypes::BATTLE; }); LOGFL("I know how %d of them works.", possibleSpells.size()); @@ -459,9 +462,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) { spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell); - if(spell->getTargetType() == spells::AimType::LOCATION) - continue; - const bool FAST = true; for(auto & target : temp.findPotentialTargets(FAST)) @@ -648,9 +648,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) DamageCache safeCopy = damageCache; DamageCache innerCache(&safeCopy); + innerCache.buildDamageCache(state, side); - if(needFullEval || !cachedAttack) + if(cachedAttack.ap && cachedAttack.waited) + { + state->makeWait(activeStack); + } + + if(needFullEval || !cachedAttack.ap) { #if BATTLE_TRACE_LEVEL >= 1 logAi->trace("Full evaluation is started due to stack speed affected."); @@ -659,29 +665,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) PotentialTargets innerTargets(activeStack, innerCache, state); BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount); + innerEvaluator.updateReachabilityMap(state); + + auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state); + if(!innerTargets.possibleAttacks.empty()) { - innerEvaluator.updateReachabilityMap(state); - auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); - ps.value = newStackAction.score; + ps.value = std::max(moveTarget.score, newStackAction.score); } else { - ps.value = 0; + ps.value = moveTarget.score; } } else { - ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state); + ps.value = scoreEvaluator.evaluateExchange(*cachedAttack.ap, cachedAttack.turn, *targets, innerCache, state); } for(const auto & unit : allUnits) { - if (!unit->isValidTarget()) + if(!unit->isValidTarget()) continue; - + auto newHealth = unit->getAvailableHealth(); auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units @@ -692,7 +700,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) auto dpsReduce = AttackPossibility::calculateDamageReduce( nullptr, - originalDefender && originalDefender->alive() ? originalDefender : unit, + originalDefender && originalDefender->alive() ? originalDefender : unit, damage, innerCache, state); @@ -719,6 +727,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) #endif } } + #if BATTLE_TRACE_LEVEL >= 1 logAi->trace("Total score: %2f", ps.value); #endif @@ -729,13 +738,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) LOGFL("Evaluation took %d ms", timer.getDiff()); - auto pscValue = [](const PossibleSpellcast &ps) -> float - { - return ps.value; - }; - auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); + auto castToPerform = *vstd::maxElementByFun(possibleCasts, [](const PossibleSpellcast & ps) -> float + { + return ps.value; + }); - if(castToPerform.value > cachedScore) + if(castToPerform.value > cachedAttack.score && !vstd::isAlmostEqual(castToPerform.value, cachedAttack.score)) { LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); BattleAction spellcast; diff --git a/AI/BattleAI/BattleEvaluator.h b/AI/BattleAI/BattleEvaluator.h index f5af918aa..0f00ffc7c 100644 --- a/AI/BattleAI/BattleEvaluator.h +++ b/AI/BattleAI/BattleEvaluator.h @@ -22,6 +22,14 @@ VCMI_LIB_NAMESPACE_END class EnemyInfo; +struct CachedAttack +{ + std::optional ap; + float score = EvaluationResult::INEFFECTIVE_SCORE; + uint8_t turn = 255; + bool waited = false; +}; + class BattleEvaluator { std::unique_ptr targets; @@ -30,11 +38,10 @@ class BattleEvaluator std::shared_ptr cb; std::shared_ptr env; bool activeActionMade = false; - std::optional cachedAttack; + CachedAttack cachedAttack; PlayerColor playerID; BattleID battleID; BattleSide side; - float cachedScore; DamageCache damageCache; float strengthRatio; int simulationTurnsCount; diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 8a72744f1..570d06c2c 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -219,9 +219,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( auto hbWaited = std::make_shared(env.get(), hb); - hbWaited->resetActiveUnit(); - hbWaited->getForUpdate(activeStack->unitId())->waiting = true; - hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true; + hbWaited->makeWait(activeStack); updateReachabilityMap(hbWaited); diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index c6dc197da..65c658414 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -502,10 +502,18 @@ ServerCallback * HypotheticBattle::getServerCallback() return serverCallback.get(); } +void HypotheticBattle::makeWait(const battle::Unit * activeStack) +{ + auto unit = getForUpdate(activeStack->unitId()); + + resetActiveUnit(); + unit->waiting = true; + unit->waitedThisTurn = true; +} + HypotheticBattle::HypotheticServerCallback::HypotheticServerCallback(HypotheticBattle * owner_) :owner(owner_) { - } void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem) diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index 118dde639..4be9d9343 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -164,6 +164,8 @@ public: int64_t getTreeVersion() const; + void makeWait(const battle::Unit * activeStack); + void resetActiveUnit() { activeUnitId = -1; From ed36b1a882d66913e599f4107474a05d22b0e5c4 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Mon, 26 Aug 2024 11:30:39 +0300 Subject: [PATCH 2/2] Battle AI: fix casting Implosion when it kills target stack, fix casting summon elementals --- AI/BattleAI/BattleEvaluator.cpp | 35 +++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index f45427e97..29deac69e 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -630,7 +630,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) auto & ps = possibleCasts[i]; #if BATTLE_TRACE_LEVEL >= 1 - logAi->trace("Evaluating %s", ps.spell->getNameTranslated()); + if(ps.dest.empty()) + logAi->trace("Evaluating %s", ps.spell->getNameTranslated()); + else + { + auto psFirst = ps.dest.front(); + auto strWhere = psFirst.unitValue ? psFirst.unitValue->getDescription() : std::to_string(psFirst.hexValue.hex); + + logAi->trace("Evaluating %s at %s", ps.spell->getNameTranslated(), strWhere); + } #endif auto state = std::make_shared(env.get(), cb->getBattle(battleID)); @@ -667,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) innerEvaluator.updateReachabilityMap(state); - auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state); + auto moveTarget = innerEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state); if(!innerTargets.possibleAttacks.empty()) { @@ -682,12 +690,22 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) } else { - ps.value = scoreEvaluator.evaluateExchange(*cachedAttack.ap, cachedAttack.turn, *targets, innerCache, state); + auto updatedAttacker = state->getForUpdate(cachedAttack.ap->attack.attacker->unitId()); + auto updatedDefender = state->getForUpdate(cachedAttack.ap->attack.defender->unitId()); + auto updatedBai = BattleAttackInfo( + updatedAttacker.get(), + updatedDefender.get(), + cachedAttack.ap->attack.chargeDistance, + cachedAttack.ap->attack.shooting); + + auto updatedAttack = AttackPossibility::evaluate(updatedBai, cachedAttack.ap->from, innerCache, state); + + ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state); } for(const auto & unit : allUnits) { - if(!unit->isValidTarget()) + if(!unit->isValidTarget(true)) continue; auto newHealth = unit->getAvailableHealth(); @@ -710,13 +728,18 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) if(ourUnit * goodEffect == 1) { - if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost())) + auto isMagical = state->getForUpdate(unit->unitId())->summoned + || unit->isClone() + || unit->isGhost(); + + if(ourUnit && goodEffect && isMagical) continue; ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); } else - ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); + // discourage AI making collateral damage with spells + ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); #if BATTLE_TRACE_LEVEL >= 1 logAi->trace(