1 /*************************************************************************** 2 * Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 * 3 * Copyright (C) 2020 * 4 * * 5 * This program is free software; you can redistribute it and/or modify * 6 * it under the terms of the GNU General Public License as published by * 7 * the Free Software Foundation; either version 2 of the License, or * 8 * (at your option) any later version. * 9 * * 10 * This program is distributed in the hope that it will be useful, * 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 13 * GNU General Public License for more details. * 14 * * 15 * You should have received a copy of the GNU General Public License * 16 * along with this program; if not, write to the * 17 * Free Software Foundation, Inc., * 18 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * 19 ***************************************************************************/ 20 21 #include "ai_normal.h" 22 #include "artifact.h" 23 #include "battle_arena.h" 24 #include "battle_army.h" 25 #include "battle_cell.h" 26 #include "battle_command.h" 27 #include "battle_tower.h" 28 #include "battle_troop.h" 29 #include "castle.h" 30 #include "difficulty.h" 31 #include "game.h" 32 #include "heroes.h" 33 #include "logging.h" 34 #include "settings.h" 35 #include "speed.h" 36 37 #include <cassert> 38 #include <cmath> 39 #include <cstdint> 40 #include <set> 41 42 using namespace Battle; 43 44 namespace AI 45 { 46 // Usual distance between units at the start of the battle is 10-14 tiles 47 // 20% of maximum value lost for every tile travelled to make sure 4 tiles difference matters 48 const double STRENGTH_DISTANCE_FACTOR = 5.0; 49 const std::vector<int> underWallsIndicies = { 7, 28, 49, 72, 95 }; 50 51 struct MeleeAttackOutcome 52 { 53 int32_t fromIndex = -1; 54 double attackValue = -INT32_MAX; 55 double positionValue = -INT32_MAX; 56 bool canAttackImmediately = false; 57 }; 58 ValueHasImproved(double primary,double primaryMax,double secondary,double secondaryMax)59 bool ValueHasImproved( double primary, double primaryMax, double secondary, double secondaryMax ) 60 { 61 return primaryMax < primary || ( secondaryMax < secondary && std::fabs( primaryMax - primary ) < 0.001 ); 62 } 63 IsOutcomeImproved(const MeleeAttackOutcome & newOutcome,const MeleeAttackOutcome & previous)64 bool IsOutcomeImproved( const MeleeAttackOutcome & newOutcome, const MeleeAttackOutcome & previous ) 65 { 66 // Composite priority criteria: 67 // Primary - Enemy is within move range and can be attacked this turn 68 // Secondary - Postion quality (to attack from, or protect friendly unit) 69 // Tertiary - Enemy unit threat 70 return ( newOutcome.canAttackImmediately && !previous.canAttackImmediately ) 71 || ( newOutcome.canAttackImmediately == previous.canAttackImmediately 72 && ValueHasImproved( newOutcome.positionValue, previous.positionValue, newOutcome.attackValue, previous.attackValue ) ); 73 } 74 BestAttackOutcome(const Arena & arena,const Unit & attacker,const Unit & defender,const Rand::DeterministicRandomGenerator & randomGenerator)75 MeleeAttackOutcome BestAttackOutcome( const Arena & arena, const Unit & attacker, const Unit & defender, const Rand::DeterministicRandomGenerator & randomGenerator ) 76 { 77 MeleeAttackOutcome bestOutcome; 78 79 Indexes around = Board::GetAroundIndexes( defender ); 80 // Shuffle to make equal quality moves a bit unpredictable 81 randomGenerator.Shuffle( around ); 82 83 for ( const int cell : around ) { 84 // Check if we can reach the target and pick best position to attack from 85 if ( !arena.hexIsPassable( cell ) ) 86 continue; 87 88 MeleeAttackOutcome current; 89 current.positionValue = Board::GetCell( cell )->GetQuality(); 90 current.attackValue = Board::OptimalAttackValue( attacker, defender, cell ); 91 current.canAttackImmediately = Board::CanAttackUnitFromPosition( attacker, defender, cell ); 92 93 // Pick target if either position has improved or unit is higher value at the same position quality 94 if ( IsOutcomeImproved( current, bestOutcome ) ) { 95 bestOutcome.attackValue = current.attackValue; 96 bestOutcome.positionValue = current.positionValue; 97 bestOutcome.fromIndex = cell; 98 bestOutcome.canAttackImmediately = current.canAttackImmediately; 99 } 100 } 101 return bestOutcome; 102 } 103 FindMoveToRetreat(const Indexes & moves,const Unit & currentUnit,const Battle::Units & enemies)104 int32_t FindMoveToRetreat( const Indexes & moves, const Unit & currentUnit, const Battle::Units & enemies ) 105 { 106 double lowestThreat = 0.0; 107 int32_t targetCell = -1; 108 109 for ( const int moveIndex : moves ) { 110 // Skip if this cell has adjacent enemies 111 if ( Board::GetCell( moveIndex )->GetQuality() ) 112 continue; 113 114 double cellThreatLevel = 0.0; 115 116 for ( const Unit * enemy : enemies ) { 117 uint32_t dist = Board::GetDistance( moveIndex, enemy->GetHeadIndex() ); 118 if ( enemy->isWide() ) { 119 const uint32_t distanceFromTail = Board::GetDistance( moveIndex, enemy->GetTailIndex() ); 120 dist = std::min( dist, distanceFromTail ); 121 } 122 123 const uint32_t range = std::max( 1u, enemy->GetMoveRange() ); 124 cellThreatLevel += enemy->GetScoreQuality( currentUnit ) * ( 1.0 - static_cast<double>( dist ) / range ); 125 } 126 127 if ( targetCell == -1 || cellThreatLevel < lowestThreat ) { 128 lowestThreat = cellThreatLevel; 129 targetCell = moveIndex; 130 } 131 } 132 return targetCell; 133 } 134 FindNextTurnAttackMove(const Indexes & moves,const Unit & currentUnit,const Battle::Units & enemies)135 int32_t FindNextTurnAttackMove( const Indexes & moves, const Unit & currentUnit, const Battle::Units & enemies ) 136 { 137 double lowestThreat = 0.0; 138 int32_t targetCell = -1; 139 140 for ( const int moveIndex : moves ) { 141 double cellThreatLevel = 0.0; 142 143 for ( const Unit * enemy : enemies ) { 144 // Archers and Flyers are always threatning, skip 145 if ( enemy->isFlying() || ( enemy->isArchers() && !enemy->isHandFighting() ) ) 146 continue; 147 148 if ( Board::GetDistance( moveIndex, enemy->GetHeadIndex() ) <= enemy->GetMoveRange() + 1 ) { 149 cellThreatLevel += enemy->GetScoreQuality( currentUnit ); 150 } 151 } 152 153 // Also allow to move up closer if there's still no threat 154 if ( targetCell == -1 || cellThreatLevel < lowestThreat || std::fabs( cellThreatLevel ) < 0.001 ) { 155 lowestThreat = cellThreatLevel; 156 targetCell = moveIndex; 157 } 158 } 159 return targetCell; 160 } 161 HeroesPreBattle(HeroBase & hero,bool isAttacking)162 void Normal::HeroesPreBattle( HeroBase & hero, bool isAttacking ) 163 { 164 if ( isAttacking ) { 165 OptimizeTroopsOrder( hero.GetArmy() ); 166 } 167 } 168 isHeroWorthSaving(const Heroes & hero) const169 bool BattlePlanner::isHeroWorthSaving( const Heroes & hero ) const 170 { 171 return hero.GetLevel() > 2 || !hero.GetBagArtifacts().empty(); 172 } 173 isCommanderCanSpellcast(const Arena & arena,const HeroBase * commander) const174 bool BattlePlanner::isCommanderCanSpellcast( const Arena & arena, const HeroBase * commander ) const 175 { 176 return commander && ( !commander->isControlHuman() || Settings::Get().BattleAutoSpellcast() ) && commander->HaveSpellBook() 177 && !commander->Modes( Heroes::SPELLCASTED ) && !arena.isSpellcastDisabled(); 178 } 179 checkRetreatCondition(const Heroes & hero) const180 bool BattlePlanner::checkRetreatCondition( const Heroes & hero ) const 181 { 182 // Retreat if remaining army strength is a fraction of enemy's 183 // Consider taking speed/turn order into account in the future 184 const double ratio = Difficulty::GetAIRetreatRatio( Game::getDifficulty() ); 185 return _considerRetreat && _myArmyStrength * ratio < _enemyArmyStrength && !hero.isControlHuman() && isHeroWorthSaving( hero ); 186 } 187 isUnitFaster(const Unit & currentUnit,const Unit & target) const188 bool BattlePlanner::isUnitFaster( const Unit & currentUnit, const Unit & target ) const 189 { 190 if ( currentUnit.isFlying() == target.isFlying() ) 191 return currentUnit.GetSpeed() > target.GetSpeed(); 192 return currentUnit.isFlying(); 193 } 194 planUnitTurn(Arena & arena,const Unit & currentUnit)195 Actions BattlePlanner::planUnitTurn( Arena & arena, const Unit & currentUnit ) 196 { 197 if ( currentUnit.Modes( SP_BERSERKER ) != 0 ) { 198 return berserkTurn( arena, currentUnit ); 199 } 200 201 Actions actions; 202 203 // Step 1. Analyze current battle state and update variables 204 analyzeBattleState( arena, currentUnit ); 205 206 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, currentUnit.GetName() << " start their turn. Side: " << _myColor ); 207 208 // Step 2. Check retreat/surrender condition 209 const Heroes * actualHero = dynamic_cast<const Heroes *>( _commander ); 210 if ( actualHero && arena.CanRetreatOpponent( _myColor ) && checkRetreatCondition( *actualHero ) ) { 211 if ( isCommanderCanSpellcast( arena, _commander ) ) { 212 // Cast maximum damage spell 213 const SpellSelection & bestSpell = selectBestSpell( arena, true ); 214 215 if ( bestSpell.spellID != -1 ) { 216 actions.emplace_back( CommandType::MSG_BATTLE_CAST, bestSpell.spellID, bestSpell.cell ); 217 } 218 } 219 220 actions.emplace_back( CommandType::MSG_BATTLE_RETREAT ); 221 actions.emplace_back( CommandType::MSG_BATTLE_END_TURN, currentUnit.GetUID() ); 222 return actions; 223 } 224 225 // Step 3. Calculate spell heuristics 226 if ( isCommanderCanSpellcast( arena, _commander ) ) { 227 const SpellSelection & bestSpell = selectBestSpell( arena, false ); 228 229 if ( bestSpell.spellID != -1 ) { 230 actions.emplace_back( CommandType::MSG_BATTLE_CAST, bestSpell.spellID, bestSpell.cell ); 231 return actions; 232 } 233 } 234 235 // Step 4. Current unit decision tree 236 const size_t actionsSize = actions.size(); 237 Battle::Arena::GetBoard()->SetPositionQuality( currentUnit ); 238 239 if ( currentUnit.isArchers() ) { 240 const Actions & archerActions = archerDecision( arena, currentUnit ); 241 actions.insert( actions.end(), archerActions.begin(), archerActions.end() ); 242 } 243 else { 244 // Melee unit decision tree (both flyers and walkers) 245 BattleTargetPair target; 246 247 // Determine unit target or cell to move to 248 if ( _defensiveTactics ) { 249 target = meleeUnitDefense( arena, currentUnit ); 250 } 251 else { 252 target = meleeUnitOffense( arena, currentUnit ); 253 } 254 255 // Melee unit final stage - add actions to the queue 256 DEBUG_LOG( DBG_BATTLE, DBG_INFO, "Melee phase end, targetCell is " << target.cell ); 257 258 if ( target.cell != -1 ) { 259 const int32_t reachableCell = arena.GetNearestReachableCell( currentUnit, target.cell ); 260 261 DEBUG_LOG( DBG_BATTLE, DBG_INFO, "Nearest reachable cell is " << reachableCell ); 262 263 if ( currentUnit.GetHeadIndex() != reachableCell ) { 264 actions.emplace_back( CommandType::MSG_BATTLE_MOVE, currentUnit.GetUID(), reachableCell ); 265 } 266 267 if ( target.unit ) { 268 actions.emplace_back( CommandType::MSG_BATTLE_ATTACK, currentUnit.GetUID(), target.unit->GetUID(), 269 Board::OptimalAttackTarget( currentUnit, *target.unit, reachableCell ), 0 ); 270 271 DEBUG_LOG( DBG_BATTLE, DBG_INFO, 272 currentUnit.GetName() << " melee offense, focus enemy " << target.unit->GetName() 273 << " threat level: " << target.unit->GetScoreQuality( currentUnit ) ); 274 } 275 } 276 // else skip 277 } 278 279 // no action was taken - skip 280 if ( actions.size() == actionsSize ) { 281 actions.emplace_back( CommandType::MSG_BATTLE_SKIP, currentUnit.GetUID(), true ); 282 } 283 284 return actions; 285 } 286 analyzeBattleState(Arena & arena,const Unit & currentUnit)287 void BattlePlanner::analyzeBattleState( Arena & arena, const Unit & currentUnit ) 288 { 289 _myColor = currentUnit.GetCurrentColor(); 290 _commander = arena.getCommander( _myColor ); 291 292 const Force & friendlyForce = arena.getForce( _myColor ); 293 const Force & enemyForce = arena.getEnemyForce( _myColor ); 294 295 // Friendly and enemy army analysis 296 _myArmyStrength = 0; 297 _enemyArmyStrength = 0; 298 _myShooterStr = 0; 299 _enemyShooterStr = 0; 300 _enemyAverageSpeed = 0; 301 _enemySpellStrength = 0; 302 _highestDamageExpected = 0; 303 _considerRetreat = false; 304 _randomGenerator = &arena.GetRandomGenerator(); 305 assert( _randomGenerator ); 306 // TODO : this pointer will dangle as we don't set it to nullptr when the Battle instance is deleted 307 308 if ( enemyForce.empty() ) 309 return; 310 311 double sumEnemyStr = 0.0; 312 for ( const Unit * unitPtr : enemyForce ) { 313 if ( !unitPtr || !unitPtr->isValid() ) 314 continue; 315 316 const Unit & unit = *unitPtr; 317 const double unitStr = unit.GetStrength(); 318 319 _enemyArmyStrength += unitStr; 320 if ( unit.isArchers() ) { 321 _enemyShooterStr += unitStr; 322 } 323 324 const int dmg = unit.CalculateMaxDamage( currentUnit ); 325 if ( dmg > _highestDamageExpected ) 326 _highestDamageExpected = dmg; 327 328 // average speed is weighted by troop strength 329 const uint32_t speed = unit.GetSpeed( false, true ); 330 _enemyAverageSpeed += speed * unitStr; 331 sumEnemyStr += unitStr; 332 } 333 334 if ( sumEnemyStr > 0.0 ) { 335 _enemyAverageSpeed /= sumEnemyStr; 336 } 337 338 uint32_t initialUnitCount = 0; 339 double sumArmyStr = 0.0; 340 for ( const Unit * unitPtr : friendlyForce ) { 341 // Do not check isValid() here to handle dead troops 342 if ( !unitPtr ) 343 continue; 344 345 const Unit & unit = *unitPtr; 346 const uint32_t count = unit.GetCount(); 347 const uint32_t dead = unit.GetDead(); 348 349 // Count all valid troops in army (both alive and dead) 350 if ( count > 0 || dead > 0 ) { 351 ++initialUnitCount; 352 } 353 354 const double unitStr = unit.GetStrength(); 355 356 // average speed is weighted by troop strength 357 const uint32_t speed = unit.GetSpeed( false, true ); 358 _myArmyAverageSpeed += speed * unitStr; 359 sumArmyStr += unitStr; 360 361 // Dead unit: trigger retreat condition and skip strength calculation 362 if ( count == 0 && dead > 0 ) { 363 _considerRetreat = true; 364 continue; 365 } 366 _myArmyStrength += unitStr; 367 if ( unit.isArchers() ) { 368 _myShooterStr += unitStr; 369 } 370 } 371 if ( sumArmyStr > 0.0 ) { 372 _myArmyAverageSpeed /= sumArmyStr; 373 } 374 _considerRetreat = _considerRetreat || initialUnitCount < 4; 375 376 // Add castle siege (and battle arena) modifiers 377 _attackingCastle = false; 378 _defendingCastle = false; 379 const Castle * castle = Arena::GetCastle(); 380 if ( castle ) { 381 const bool attackerIgnoresCover = arena.GetForce1().GetCommander()->hasArtifact( Artifact::GOLDEN_BOW ); 382 383 // TODO: verify that GetScoreQuality method always returns positive values here. Most likely for neutral castle it returns negative. 384 auto getTowerStrength = [¤tUnit]( const Tower * tower ) { return ( tower && tower->isValid() ) ? tower->GetScoreQuality( currentUnit ) : 0; }; 385 386 double towerStr = getTowerStrength( Arena::GetTower( TWR_CENTER ) ); 387 towerStr += getTowerStrength( Arena::GetTower( TWR_LEFT ) ); 388 towerStr += getTowerStrength( Arena::GetTower( TWR_RIGHT ) ); 389 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "- Castle strength: " << towerStr ); 390 391 if ( _myColor == castle->GetColor() ) { 392 _defendingCastle = true; 393 _myShooterStr += towerStr; 394 if ( !attackerIgnoresCover ) 395 _enemyShooterStr /= 2; 396 } 397 else { 398 _attackingCastle = true; 399 _enemyShooterStr += towerStr; 400 if ( !attackerIgnoresCover ) 401 _myShooterStr /= 2; 402 } 403 } 404 405 // TODO: replace this hacky code for archers 406 // Calculate each hero spell strength and add it to shooter values after castle modifiers were applied 407 if ( _commander && _myShooterStr > 1 ) { 408 _myShooterStr += _commander->GetSpellcastStrength( _myArmyStrength ); 409 } 410 const HeroBase * enemyCommander = arena.getEnemyCommander( _myColor ); 411 if ( enemyCommander ) { 412 _enemySpellStrength = enemyCommander->GetSpellcastStrength( _enemyArmyStrength ); 413 _enemyShooterStr += _enemySpellStrength; 414 } 415 416 double overPowerRatio = 10; // for melee creatures 417 if ( currentUnit.isFlying() ) { 418 overPowerRatio = 6; 419 } 420 if ( _defendingCastle ) { 421 overPowerRatio /= 2; // don't make shooters to kill us. 422 } 423 424 // When we have in X times stronger army than the enemy we could consider it as an overpowered and we most likely will win. 425 const bool myOverpoweredArmy = _myArmyStrength > _enemyArmyStrength * overPowerRatio; 426 const double enemyArcherRatio = _enemyShooterStr / _enemyArmyStrength; 427 428 double enemyArcherThreshold = 0.75; 429 if ( _defendingCastle ) { 430 // Don't make shooters to kill us while we are standing in the castle. 431 enemyArcherThreshold /= 2; 432 } 433 434 _defensiveTactics = enemyArcherRatio < enemyArcherThreshold && ( _defendingCastle || _myShooterStr > _enemyShooterStr ) && !myOverpoweredArmy; 435 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, 436 "Tactic " << _defensiveTactics << " chosen. Archers: " << _myShooterStr << ", vs enemy " << _enemyShooterStr << " ratio is " << enemyArcherRatio ); 437 } 438 archerDecision(Arena & arena,const Unit & currentUnit) const439 Actions BattlePlanner::archerDecision( Arena & arena, const Unit & currentUnit ) const 440 { 441 Actions actions; 442 const Units enemies( arena.getEnemyForce( _myColor ), true ); 443 BattleTargetPair target; 444 445 if ( currentUnit.isHandFighting() ) { 446 // Current ranged unit is blocked by the enemy 447 448 // Force archer to fight back by setting initial expectation to lowest possible (if we're losing battle) 449 int bestOutcome = ( _myArmyStrength < _enemyArmyStrength ) ? -_highestDamageExpected : 0; 450 451 const Indexes & adjacentEnemies = Board::GetAdjacentEnemies( currentUnit ); 452 for ( const int cell : adjacentEnemies ) { 453 const Unit * enemy = Board::GetCell( cell )->GetUnit(); 454 if ( enemy ) { 455 const int archerMeleeDmg = currentUnit.GetDamage( *enemy ); 456 const int damageDiff = archerMeleeDmg - enemy->CalculateRetaliationDamage( archerMeleeDmg ); 457 458 if ( bestOutcome < damageDiff ) { 459 bestOutcome = damageDiff; 460 target.unit = enemy; 461 target.cell = cell; 462 } 463 } 464 else { 465 DEBUG_LOG( DBG_BATTLE, DBG_WARN, "Board::GetAdjacentEnemies returned a cell " << cell << " that does not contain a unit!" ); 466 } 467 } 468 469 if ( target.unit && target.cell != -1 ) { 470 // Melee attack selected target 471 DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " archer deciding to fight back: " << bestOutcome ); 472 actions.emplace_back( CommandType::MSG_BATTLE_ATTACK, currentUnit.GetUID(), target.unit->GetUID(), target.cell, 0 ); 473 } 474 else { 475 // Kiting enemy: Search for a safe spot unit can move to 476 target.cell = FindMoveToRetreat( arena.getAllAvailableMoves( currentUnit.GetMoveRange() ), currentUnit, enemies ); 477 478 if ( target.cell != -1 ) { 479 DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " archer kiting enemy, moving to " << target.cell ); 480 481 const int32_t reachableCell = arena.GetNearestReachableCell( currentUnit, target.cell ); 482 483 DEBUG_LOG( DBG_BATTLE, DBG_INFO, "Nearest reachable cell is " << reachableCell ); 484 485 if ( currentUnit.GetHeadIndex() != reachableCell ) { 486 actions.emplace_back( CommandType::MSG_BATTLE_MOVE, currentUnit.GetUID(), reachableCell ); 487 } 488 } 489 } 490 // Worst case scenario - Skip turn 491 } 492 else { 493 // Normal ranged attack: focus the highest value unit 494 double highestStrength = 0; 495 496 for ( const Unit * enemy : enemies ) { 497 double attackPriority = enemy->GetScoreQuality( currentUnit ); 498 499 if ( currentUnit.isAbilityPresent( fheroes2::MonsterAbilityType::AREA_SHOT ) ) { 500 // TODO: update logic to handle tail case as well. Right now archers always shoot to head. 501 const Indexes around = Board::GetAroundIndexes( enemy->GetHeadIndex() ); 502 std::set<const Unit *> targetedUnits; 503 504 for ( const int32_t cellId : around ) { 505 const Unit * monsterOnCell = Board::GetCell( cellId )->GetUnit(); 506 if ( monsterOnCell != nullptr ) { 507 targetedUnits.emplace( monsterOnCell ); 508 } 509 } 510 511 for ( const Unit * monster : targetedUnits ) { 512 if ( enemy != monster ) { 513 // No need to recalculate for the same monster. 514 attackPriority += monster->GetScoreQuality( currentUnit ); 515 } 516 } 517 } 518 519 if ( highestStrength < attackPriority && attackPriority > 0 ) { 520 highestStrength = attackPriority; 521 target.unit = enemy; 522 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "- Set priority on " << enemy->GetName() << " value " << attackPriority ); 523 } 524 } 525 526 if ( target.unit ) { 527 actions.emplace_back( CommandType::MSG_BATTLE_ATTACK, currentUnit.GetUID(), target.unit->GetUID(), target.unit->GetHeadIndex(), 0 ); 528 529 DEBUG_LOG( DBG_BATTLE, DBG_INFO, 530 currentUnit.GetName() << " archer focusing enemy " << target.unit->GetName() 531 << " threat level: " << target.unit->GetScoreQuality( currentUnit ) ); 532 } 533 } 534 535 return actions; 536 } 537 meleeUnitOffense(Arena & arena,const Unit & currentUnit) const538 BattleTargetPair BattlePlanner::meleeUnitOffense( Arena & arena, const Unit & currentUnit ) const 539 { 540 BattleTargetPair target; 541 const Units enemies( arena.getEnemyForce( _myColor ), true ); 542 543 double attackHighestValue = -_enemyArmyStrength; 544 double attackPositionValue = -_enemyArmyStrength; 545 546 for ( const Unit * enemy : enemies ) { 547 const MeleeAttackOutcome & outcome = BestAttackOutcome( arena, currentUnit, *enemy, *_randomGenerator ); 548 549 if ( outcome.canAttackImmediately && ValueHasImproved( outcome.positionValue, attackPositionValue, outcome.attackValue, attackHighestValue ) ) { 550 attackHighestValue = outcome.attackValue; 551 attackPositionValue = outcome.positionValue; 552 target.cell = outcome.fromIndex; 553 target.unit = enemy; 554 } 555 } 556 557 // For walking units that don't have a target within reach, pick based on distance priority 558 if ( target.unit == nullptr ) { 559 const uint32_t currentUnitMoveRange = currentUnit.GetMoveRange(); 560 const double attackDistanceModifier = _enemyArmyStrength / STRENGTH_DISTANCE_FACTOR; 561 double maxMovePriority = attackDistanceModifier * ARENASIZE * -1; 562 563 for ( const Unit * enemy : enemies ) { 564 // move node pair consists of move hex index and distance 565 const std::pair<int, uint32_t> move = arena.CalculateMoveToUnit( *enemy ); 566 567 if ( move.first == -1 ) // Skip unit if no path found 568 continue; 569 570 // Do not chase after faster units that might kite away and avoid engagement 571 const uint32_t distance = ( !enemy->isArchers() && isUnitFaster( *enemy, currentUnit ) ) ? move.second + ARENAW + ARENAH : move.second; 572 573 const double unitPriority = enemy->GetScoreQuality( currentUnit ) - distance * attackDistanceModifier; 574 if ( unitPriority > maxMovePriority ) { 575 maxMovePriority = unitPriority; 576 577 const Indexes & path = arena.CalculateTwoMoveOverlap( move.first, currentUnitMoveRange ); 578 if ( !path.empty() ) { 579 target.cell = FindNextTurnAttackMove( path, currentUnit, enemies ); 580 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, "Going after target " << enemy->GetName() << " stopping at " << target.cell ); 581 } 582 else { 583 target.cell = move.first; 584 } 585 } 586 } 587 } 588 else { 589 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, currentUnit.GetName() << " is attacking " << target.unit->GetName() << " at " << target.cell ); 590 } 591 592 // Walkers: move closer to the castle walls during siege 593 if ( _attackingCastle && target.cell == -1 ) { 594 uint32_t shortestDist = UINT32_MAX; 595 596 for ( const int wallIndex : underWallsIndicies ) { 597 if ( !arena.hexIsPassable( wallIndex ) ) { 598 continue; 599 } 600 601 const uint32_t dist = arena.CalculateMoveDistance( wallIndex ); 602 if ( dist < shortestDist ) { 603 shortestDist = dist; 604 target.cell = wallIndex; 605 } 606 } 607 DEBUG_LOG( DBG_BATTLE, DBG_INFO, "Walker unit moving towards castle walls " << currentUnit.GetName() << " cell " << target.cell ); 608 } 609 610 return target; 611 } 612 meleeUnitDefense(Arena & arena,const Unit & currentUnit) const613 BattleTargetPair BattlePlanner::meleeUnitDefense( Arena & arena, const Unit & currentUnit ) const 614 { 615 BattleTargetPair target; 616 617 const Units friendly( arena.getForce( _myColor ), true ); 618 const Units enemies( arena.getEnemyForce( _myColor ), true ); 619 620 const int myHeadIndex = currentUnit.GetHeadIndex(); 621 622 const double defenceDistanceModifier = _myArmyStrength / STRENGTH_DISTANCE_FACTOR; 623 624 // 1. Check if there's a target within our half of the battlefield 625 MeleeAttackOutcome attackOption; 626 for ( const Unit * enemy : enemies ) { 627 const MeleeAttackOutcome & outcome = BestAttackOutcome( arena, currentUnit, *enemy, *_randomGenerator ); 628 629 // Allow to move only within our half of the battlefield. If in castle make sure to stay inside. 630 if ( ( !_defendingCastle && Board::DistanceFromOriginX( outcome.fromIndex, currentUnit.isReflect() ) > ARENAW / 2 ) 631 || ( _defendingCastle && !Board::isCastleIndex( outcome.fromIndex ) ) ) 632 continue; 633 634 if ( IsOutcomeImproved( outcome, attackOption ) ) { 635 attackOption.attackValue = outcome.attackValue; 636 attackOption.positionValue = outcome.positionValue; 637 attackOption.canAttackImmediately = outcome.canAttackImmediately; 638 target.cell = outcome.fromIndex; 639 target.unit = outcome.canAttackImmediately ? enemy : nullptr; 640 } 641 } 642 643 // 2. Check if our archer units are under threat - overwrite target and protect 644 MeleeAttackOutcome protectOption; 645 for ( const Unit * unitToDefend : friendly ) { 646 if ( unitToDefend->GetUID() == currentUnit.GetUID() || !unitToDefend->isArchers() ) { 647 continue; 648 } 649 650 const std::pair<int, uint32_t> move = arena.CalculateMoveToUnit( *unitToDefend ); 651 const uint32_t distanceToUnit = ( move.first != -1 ) ? move.second : Board::GetDistance( myHeadIndex, unitToDefend->GetHeadIndex() ); 652 const double archerValue = unitToDefend->GetStrength() - distanceToUnit * defenceDistanceModifier; 653 654 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, unitToDefend->GetName() << " archer value " << archerValue << " distance: " << distanceToUnit ); 655 656 // 3. Search for enemy units blocking our archers within range move 657 const Indexes & adjacentEnemies = Board::GetAdjacentEnemies( *unitToDefend ); 658 for ( const int cell : adjacentEnemies ) { 659 const Unit * enemy = Board::GetCell( cell )->GetUnit(); 660 if ( !enemy ) { 661 DEBUG_LOG( DBG_BATTLE, DBG_WARN, "Board::GetAdjacentEnemies returned a cell " << cell << " that does not contain a unit!" ); 662 continue; 663 } 664 665 MeleeAttackOutcome outcome = BestAttackOutcome( arena, currentUnit, *enemy, *_randomGenerator ); 666 outcome.positionValue = archerValue; 667 668 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, " - Found enemy, cell " << cell << " threat " << outcome.attackValue ); 669 670 if ( IsOutcomeImproved( outcome, protectOption ) ) { 671 protectOption.attackValue = outcome.attackValue; 672 protectOption.positionValue = archerValue; 673 protectOption.canAttackImmediately = outcome.canAttackImmediately; 674 target.cell = outcome.fromIndex; 675 target.unit = outcome.canAttackImmediately ? enemy : nullptr; 676 677 DEBUG_LOG( DBG_BATTLE, DBG_TRACE, " - Target selected " << enemy->GetName() << " cell " << target.cell << " archer value " << archerValue ); 678 } 679 } 680 681 // 4. No enemies found anywhere - move in closer to the friendly ranged unit 682 if ( !target.unit && protectOption.positionValue < archerValue ) { 683 target.cell = move.first; 684 protectOption.positionValue = archerValue; 685 } 686 } 687 688 if ( target.unit ) { 689 DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " defending against " << target.unit->GetName() << " threat level: " << protectOption.attackValue ); 690 } 691 else if ( target.cell != -1 ) { 692 DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " protecting friendly archer, moving to " << target.cell ); 693 } 694 695 return target; 696 } 697 berserkTurn(Arena & arena,const Unit & currentUnit) const698 Actions BattlePlanner::berserkTurn( Arena & arena, const Unit & currentUnit ) const 699 { 700 assert( currentUnit.Modes( SP_BERSERKER ) ); 701 Actions actions; 702 703 Board & board = *Arena::GetBoard(); 704 const uint32_t currentUnitUID = currentUnit.GetUID(); 705 706 const std::vector<Unit *> nearestUnits = board.GetNearestTroops( ¤tUnit, std::vector<Unit *>() ); 707 if ( !nearestUnits.empty() ) { 708 for ( const Unit * targetUnit : nearestUnits ) { 709 const uint32_t targetUnitUID = targetUnit->GetUID(); 710 const int32_t targetUnitHead = targetUnit->GetHeadIndex(); 711 if ( currentUnit.isArchers() && !currentUnit.isHandFighting() ) { 712 actions.emplace_back( CommandType::MSG_BATTLE_ATTACK, currentUnitUID, targetUnitUID, targetUnitHead, 0 ); 713 break; 714 } 715 else { 716 int targetCell = -1; 717 const Indexes & around = Board::GetAroundIndexes( *targetUnit ); 718 for ( const int cell : around ) { 719 if ( arena.hexIsPassable( cell ) ) { 720 targetCell = cell; 721 break; 722 } 723 } 724 725 if ( targetCell != -1 ) { 726 DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " is under Berserk spell, moving to " << targetCell ); 727 728 const int32_t reachableCell = arena.GetNearestReachableCell( currentUnit, targetCell ); 729 730 DEBUG_LOG( DBG_BATTLE, DBG_INFO, "Nearest reachable cell is " << reachableCell ); 731 732 if ( currentUnit.GetHeadIndex() != reachableCell ) { 733 actions.emplace_back( CommandType::MSG_BATTLE_MOVE, currentUnitUID, reachableCell ); 734 } 735 736 // Attack only if target unit is reachable and can be attacked 737 if ( Board::CanAttackUnitFromPosition( currentUnit, *targetUnit, reachableCell ) ) { 738 actions.emplace_back( CommandType::MSG_BATTLE_ATTACK, currentUnitUID, targetUnitUID, targetUnitHead, 0 ); 739 740 DEBUG_LOG( DBG_BATTLE, DBG_INFO, currentUnit.GetName() << " melee offense, focus enemy " << targetUnit->GetName() ); 741 } 742 743 break; 744 } 745 } 746 } 747 } 748 749 actions.emplace_back( CommandType::MSG_BATTLE_END_TURN, currentUnitUID ); 750 return actions; 751 } 752 BattleTurn(Arena & arena,const Unit & currentUnit,Actions & actions)753 void Normal::BattleTurn( Arena & arena, const Unit & currentUnit, Actions & actions ) 754 { 755 Board * board = Arena::GetBoard(); 756 757 board->Reset(); 758 board->SetScanPassability( currentUnit ); 759 760 const Actions & plannedActions = _battlePlanner.planUnitTurn( arena, currentUnit ); 761 actions.insert( actions.end(), plannedActions.begin(), plannedActions.end() ); 762 // Do not end the turn if we only cast a spell 763 if ( plannedActions.size() != 1 || !plannedActions.front().isType( CommandType::MSG_BATTLE_CAST ) ) 764 actions.emplace_back( CommandType::MSG_BATTLE_END_TURN, currentUnit.GetUID() ); 765 } 766 } 767