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 = [&currentUnit]( 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( &currentUnit, 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