diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index a20899f11..6c43fe7df 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -213,7 +213,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) moveTarget.score, moveTarget.scorePerTurn); - return goTowardsNearest(stack, moveTarget.positions); + return goTowardsNearest(stack, moveTarget.positions, *targets); } else { @@ -235,7 +235,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); else - return goTowardsNearest(stack, brokenWallMoat); + return goTowardsNearest(stack, brokenWallMoat, *targets); } } @@ -249,7 +249,7 @@ uint64_t timeElapsed(std::chrono::time_point return std::chrono::duration_cast(end - start).count(); } -BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector hexes) +BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector hexes, const PotentialTargets & targets) { auto reachability = cb->getBattle(battleID)->getReachability(stack); auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); @@ -272,7 +272,27 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector { if(vstd::contains(avHexes, hex)) { - return BattleAction::makeMove(stack, hex); + auto additionalScore = 0; + std::optional attackOnTheWay; + + for(auto & target : targets.possibleAttacks) + { + if(!target.attack.shooting && target.from == hex && target.attackValue() > additionalScore) + { + additionalScore = target.attackValue(); + attackOnTheWay = target; + } + } + + if(attackOnTheWay) + { + activeActionMade = true; + return BattleAction::makeMeleeAttack(stack, attackOnTheWay->attack.defender->getPosition(), attackOnTheWay->from); + } + else + { + return BattleAction::makeMove(stack, hex); + } } if(stack->coversPos(hex)) diff --git a/AI/BattleAI/BattleEvaluator.h b/AI/BattleAI/BattleEvaluator.h index 1c71c45f6..c3342df7d 100644 --- a/AI/BattleAI/BattleEvaluator.h +++ b/AI/BattleAI/BattleEvaluator.h @@ -43,7 +43,7 @@ public: bool attemptCastingSpell(const CStack * stack); bool canCastSpell(); std::optional findBestCreatureSpell(const CStack * stack); - BattleAction goTowardsNearest(const CStack * stack, std::vector hexes); + BattleAction goTowardsNearest(const CStack * stack, std::vector hexes, const PotentialTargets & targets); std::vector getBrokenWallMoatHexes() const; void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only void print(const std::string & text) const; diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 79eea1e83..758d732d2 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -277,6 +277,36 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( return result; } +ReachabilityInfo getReachabilityWithEnemyBypass( + const battle::Unit * activeStack, + DamageCache & damageCache, + std::shared_ptr state) +{ + ReachabilityInfo::Parameters params(activeStack, activeStack->getPosition()); + + if(!params.flying) + { + for(const auto * unit : state->battleAliveUnits()) + { + if(unit->unitSide() == activeStack->unitSide()) + continue; + + auto dmg = damageCache.getOriginalDamage(activeStack, unit, state); + auto turnsToKill = unit->getAvailableHealth() / dmg + 1; + + vstd::amin(turnsToKill, 100); + + for(auto & hex : unit->getHexes()) + if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns + params.destructibleEnemyTurns[hex] = turnsToKill * unit->getMovementRange(); + } + + params.bypassEnemyStacks = true; + } + + return state->getReachability(params); +} + MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( const battle::Unit * activeStack, PotentialTargets & targets, @@ -286,6 +316,8 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( MoveTarget result; BattleExchangeVariant ev; + logAi->trace("Find move towards unreachable. Enemies count %d", targets.unreachableEnemies.size()); + if(targets.unreachableEnemies.empty()) return result; @@ -296,17 +328,17 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( updateReachabilityMap(hb); - auto dists = cb->getReachability(activeStack); + auto dists = getReachabilityWithEnemyBypass(activeStack, damageCache, hb); + auto flying = activeStack->hasBonusOfType(BonusType::FLYING); for(const battle::Unit * enemy : targets.unreachableEnemies) { - std::vector adjacentStacks = getAdjacentUnits(enemy); - auto closestStack = *vstd::minElementByFun(adjacentStacks, [&](const battle::Unit * u) -> int64_t - { - return dists.distToNearestNeighbour(activeStack, u) * 100000 - activeStack->getTotalHealth(); - }); + logAi->trace( + "Checking movement towards %d of %s", + enemy->getCount(), + enemy->creatureId().toCreature()->getNameSingularTranslated()); - auto distance = dists.distToNearestNeighbour(activeStack, closestStack); + auto distance = dists.distToNearestNeighbour(activeStack, enemy); if(distance >= GameConstants::BFIELD_SIZE) continue; @@ -315,30 +347,94 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( continue; auto turnsToRich = (distance - 1) / speed + 1; - auto hexes = closestStack->getSurroundingHexes(); - auto enemySpeed = closestStack->getMovementRange(); + auto hexes = enemy->getSurroundingHexes(); + auto enemySpeed = enemy->getMovementRange(); auto speedRatio = speed / static_cast(enemySpeed); auto multiplier = speedRatio > 1 ? 1 : speedRatio; if(enemy->canShoot()) multiplier *= 1.5f; - for(auto hex : hexes) + for(auto & hex : hexes) { // FIXME: provide distance info for Jousting bonus - auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack)); + auto bai = BattleAttackInfo(activeStack, enemy, 0, cb->battleCanShoot(activeStack)); auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb); 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 scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(multiplier / turnsToRich), score.ourDamageReduce); + auto scorePerTurn = BattleScore(score.enemyDamageReduce * multiplier / turnsToRich, score.ourDamageReduce); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.scorePerTurn, scoreValue(scorePerTurn)); +#endif if(result.scorePerTurn < scoreValue(scorePerTurn)) { result.scorePerTurn = scoreValue(scorePerTurn); result.score = scoreValue(score); - result.positions = closestStack->getAttackableHexes(activeStack); + result.positions.clear(); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("New high score"); +#endif + + for(BattleHex enemyHex : enemy->getAttackableHexes(activeStack)) + { + while(!flying && dists.distances[enemyHex] > speed) + { + enemyHex = dists.predecessors.at(enemyHex); + if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK) + { + auto defenderToBypass = hb->battleGetUnitByPos(enemyHex); + + if(defenderToBypass) + { +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Found target to bypass at %d", enemyHex.hex); +#endif + + auto attackHex = dists.predecessors[enemyHex]; + auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack)); + auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb); + + auto adjacentStacks = getAdjacentUnits(enemy); + + adjacentStacks.push_back(defenderToBypass); + vstd::removeDuplicates(adjacentStacks); + + auto bypassScore = calculateExchange( + attackBypass, + dists.distances[attackHex], + targets, + damageCache, + hb, + adjacentStacks); + + if(scoreValue(bypassScore) > result.score) + { + auto newMultiplier = multiplier * speed * turnsToRich / dists.distances[attackHex]; + + result.score = scoreValue(bypassScore); + + scorePerTurn = BattleScore( + score.enemyDamageReduce * newMultiplier, + score.ourDamageReduce); + + result.scorePerTurn = scoreValue(scorePerTurn); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("New high score after bypass %f", scoreValue(scorePerTurn)); +#endif + } + } + } + } + + result.positions.push_back(enemyHex); + } + result.cachedAttack = attack; result.turnsToRich = turnsToRich; } @@ -382,7 +478,8 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( const AttackPossibility & ap, uint8_t turn, PotentialTargets & targets, - std::shared_ptr hb) const + std::shared_ptr hb, + std::vector additionalUnits) const { ReachabilityData result; @@ -390,7 +487,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( if(!ap.attack.shooting) hexes.push_back(ap.from); - std::vector allReachableUnits; + std::vector allReachableUnits = additionalUnits; for(auto hex : hexes) { @@ -432,7 +529,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( for(auto unit : allReachableUnits) { - auto accessible = !unit->canShoot(); + auto accessible = !unit->canShoot() || vstd::contains(additionalUnits, unit); if(!accessible) { @@ -494,7 +591,8 @@ BattleScore BattleExchangeEvaluator::calculateExchange( uint8_t turn, PotentialTargets & targets, DamageCache & damageCache, - std::shared_ptr hb) const + std::shared_ptr hb, + std::vector additionalUnits) const { #if BATTLE_TRACE_LEVEL>=1 logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex); @@ -513,7 +611,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange( if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) enemyStacks.push_back(ap.attack.defender); - ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb); + ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb, additionalUnits); if(exchangeUnits.units.empty()) { diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 7cb8d08c5..e3d8f95d7 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -139,7 +139,8 @@ private: uint8_t turn, PotentialTargets & targets, DamageCache & damageCache, - std::shared_ptr hb) const; + std::shared_ptr hb, + std::vector additionalUnits = {}) const; bool canBeHitThisTurn(const AttackPossibility & ap); @@ -171,7 +172,8 @@ public: const AttackPossibility & ap, uint8_t turn, PotentialTargets & targets, - std::shared_ptr hb) const; + std::shared_ptr hb, + std::vector additionalUnits = {}) const; bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position); diff --git a/lib/battle/AccessibilityInfo.cpp b/lib/battle/AccessibilityInfo.cpp index 44e093f99..22fa5196e 100644 --- a/lib/battle/AccessibilityInfo.cpp +++ b/lib/battle/AccessibilityInfo.cpp @@ -18,9 +18,19 @@ VCMI_LIB_NAMESPACE_BEGIN bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, BattleSide side) const { //at(otherHex) != EAccessibility::ACCESSIBLE && (at(otherHex) != EAccessibility::GATE || side != BattleSide::DEFENDER) - if(at(tile) != EAccessibility::ACCESSIBLE) - if(at(tile) != EAccessibility::GATE || side != BattleSide::DEFENDER) + auto accessibility = at(tile); + + if(accessibility == EAccessibility::ALIVE_STACK) + { + auto destructible = destructibleEnemyTurns.find(tile); + + return destructible != destructibleEnemyTurns.end(); + } + + if(accessibility != EAccessibility::ACCESSIBLE) + if(accessibility != EAccessibility::GATE || side != BattleSide::DEFENDER) return false; + return true; } diff --git a/lib/battle/AccessibilityInfo.h b/lib/battle/AccessibilityInfo.h index f9604a490..1352c4da2 100644 --- a/lib/battle/AccessibilityInfo.h +++ b/lib/battle/AccessibilityInfo.h @@ -35,6 +35,8 @@ using TAccessibilityArray = std::array destructibleEnemyTurns; + public: bool accessible(BattleHex tile, const battle::Unit * stack) const; //checks for both tiles if stack is double wide bool accessible(BattleHex tile, bool doubleWide, BattleSide side) const; //checks for both tiles if stack is double wide diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 53bec15b1..37900ebf9 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1052,16 +1052,29 @@ ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibi continue; const int costToNeighbour = ret.distances[curHex.hex] + 1; + for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex]) { if(neighbour.isValid()) { + auto additionalCost = 0; + + if(params.bypassEnemyStacks) + { + auto enemyToBypass = params.destructibleEnemyTurns.find(neighbour); + + if(enemyToBypass != params.destructibleEnemyTurns.end()) + { + additionalCost = enemyToBypass->second; + } + } + const int costFoundSoFar = ret.distances[neighbour.hex]; - if(accessibleCache[neighbour.hex] && costToNeighbour < costFoundSoFar) + if(accessibleCache[neighbour.hex] && costToNeighbour + additionalCost < costFoundSoFar) { hexq.push(neighbour); - ret.distances[neighbour.hex] = costToNeighbour; + ret.distances[neighbour.hex] = costToNeighbour + additionalCost; ret.predecessors[neighbour.hex] = curHex; } } @@ -1236,7 +1249,13 @@ ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Pa if(params.flying) return getFlyingReachability(params); else - return makeBFS(getAccessibility(params.knownAccessible), params); + { + auto accessibility = getAccessibility(params.knownAccessible); + + accessibility.destructibleEnemyTurns = params.destructibleEnemyTurns; + + return makeBFS(accessibility, params); + } } ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters ¶ms) const diff --git a/lib/battle/ReachabilityInfo.h b/lib/battle/ReachabilityInfo.h index de4fe21c4..f0c5ed948 100644 --- a/lib/battle/ReachabilityInfo.h +++ b/lib/battle/ReachabilityInfo.h @@ -29,7 +29,9 @@ struct DLL_LINKAGE ReachabilityInfo bool doubleWide = false; bool flying = false; bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes + bool bypassEnemyStacks = false; // in case of true will count amount of turns needed to kill enemy and thus move forward std::vector knownAccessible; //hexes that will be treated as accessible, even if they're occupied by stack (by default - tiles occupied by stack we do reachability for, so it doesn't block itself) + std::map destructibleEnemyTurns; // hom many turns it is needed to kill enemy on specific hex BattleHex startPosition; //assumed position of stack BattleSide perspective = BattleSide::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side