Skip to content

Commit

Permalink
Improve AI pathfinding logic regarding protected and guarded tiles (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
oleg-derevenetz authored Jul 19, 2023
1 parent 52f6cf2 commit 16ae0d9
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 236 deletions.
1 change: 0 additions & 1 deletion src/fheroes2/ai/ai.h
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ namespace AI
void HeroesAction( Heroes & hero, const int32_t dst_index );
void HeroesMove( Heroes & hero );
void HeroesCastDimensionDoor( Heroes & hero, const int32_t targetIndex );
void HeroesCastTownPortal( Heroes & hero, const int32_t targetIndex );
bool HeroesCastAdventureSpell( Heroes & hero, const Spell & spell );

// functionality in ai_common.cpp
Expand Down
76 changes: 39 additions & 37 deletions src/fheroes2/ai/ai_hero_action.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,44 @@ namespace
return true;
}

void AITownPortal( Heroes & hero, const int32_t targetIndex )
{
assert( !hero.Modes( Heroes::PATROL ) && Maps::isValidAbsIndex( targetIndex ) );
#ifndef NDEBUG
const Castle * targetCastle = world.getCastleEntrance( Maps::GetPoint( targetIndex ) );
#endif
assert( targetCastle && targetCastle->GetHero() == nullptr );

const Spell spellToUse = [&hero, targetIndex]() {
const Castle * nearestCastle = fheroes2::getNearestCastleTownGate( hero );
assert( nearestCastle != nullptr );

if ( nearestCastle->GetIndex() == targetIndex && hero.CanCastSpell( Spell::TOWNGATE ) ) {
return Spell::TOWNGATE;
}

return Spell::TOWNPORTAL;
}();

assert( hero.CanCastSpell( spellToUse ) );

if ( AIHeroesShowAnimation( hero, AIGetAllianceColors() ) ) {
Interface::AdventureMap::Get().getGameArea().SetCenter( hero.GetCenter() );
hero.FadeOut();
}

hero.Move2Dest( targetIndex );
hero.SpellCasted( spellToUse );
hero.GetPath().Reset();

if ( AIHeroesShowAnimation( hero, AIGetAllianceColors() ) ) {
Interface::AdventureMap::Get().getGameArea().SetCenter( hero.GetCenter() );
hero.FadeIn();
}

AI::Get().HeroesActionComplete( hero, targetIndex, hero.GetMapsObject() );
}

void AIBattleLose( Heroes & hero, const Battle::Result & res, bool attacker, const fheroes2::Point * centerOn = nullptr, const bool playSound = false )
{
const uint32_t reason = attacker ? res.AttackerResult() : res.DefenderResult();
Expand Down Expand Up @@ -1976,7 +2014,7 @@ namespace AI
const int32_t targetIndex = step.GetIndex();

if ( step.GetFrom() != targetIndex && world.GetTiles( targetIndex ).GetObject() == MP2::OBJ_CASTLE ) {
HeroesCastTownPortal( hero, targetIndex );
AITownPortal( hero, targetIndex );
}
else if ( MP2::isActionObject( hero.GetMapsObject(), hero.isShipMaster() ) ) {
// use the action object hero is standing on (Stone Liths)
Expand Down Expand Up @@ -2027,42 +2065,6 @@ namespace AI
hero.ActionNewPosition( false );
}

void HeroesCastTownPortal( Heroes & hero, const int32_t targetIndex )
{
if ( !Maps::isValidAbsIndex( targetIndex ) || hero.isShipMaster() ) {
return;
}

Spell spellToUse( Spell::TOWNPORTAL );

// check if we can cast Town Gate instead
const Spell townGate( Spell::TOWNGATE );
const Castle * nearestCastle = fheroes2::getNearestCastleTownGate( hero );
if ( nearestCastle && nearestCastle->GetIndex() == targetIndex && hero.HaveSpell( townGate ) ) {
spellToUse = townGate;
}

if ( !hero.CanCastSpell( spellToUse ) ) {
return;
}

if ( AIHeroesShowAnimation( hero, AIGetAllianceColors() ) ) {
Interface::AdventureMap::Get().getGameArea().SetCenter( hero.GetCenter() );
hero.FadeOut();
}

hero.Move2Dest( targetIndex );
hero.SpellCasted( spellToUse );
hero.GetPath().Reset();

if ( AIHeroesShowAnimation( hero, AIGetAllianceColors() ) ) {
Interface::AdventureMap::Get().getGameArea().SetCenter( hero.GetCenter() );
hero.FadeIn();
}

AI::Get().HeroesActionComplete( hero, targetIndex, hero.GetMapsObject() );
}

bool HeroesCastAdventureSpell( Heroes & hero, const Spell & spell )
{
if ( !hero.CanCastSpell( spell ) )
Expand Down
123 changes: 66 additions & 57 deletions src/fheroes2/ai/normal/ai_normal_hero.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ namespace
}

const double advantage = hero.isLosingGame() ? AI::ARMY_ADVANTAGE_DESPERATE : AI::ARMY_ADVANTAGE_MEDIUM;
const double castleStrength = castle->GetGarrisonStrength( &hero ) * advantage;
const double castleStrength = castle->GetGarrisonStrength( hero ) * advantage;

return heroArmyStrength > castleStrength;
}
Expand Down Expand Up @@ -605,7 +605,7 @@ namespace
}

case MP2::OBJ_CASTLE:
return AIShouldVisitCastle( hero, index, heroArmyStrength ) || ai.isPriorityTask( index );
return AIShouldVisitCastle( hero, index, heroArmyStrength );

case MP2::OBJ_JAIL:
return kingdom.GetHeroes().size() < Kingdom::GetMaxHeroes();
Expand Down Expand Up @@ -1791,14 +1791,14 @@ namespace AI
}

if ( isObjectReachableAtThisTurn ) {
if ( castle->GetGarrisonStrength( &hero ) > heroStrength / 2 ) {
if ( castle->GetGarrisonStrength( hero ) > heroStrength / 2 ) {
value -= dangerousTaskPenalty / 4;
}
else {
value -= dangerousTaskPenalty / 10;
}
}
else if ( castle->GetGarrisonStrength( &hero ) > heroStrength / 2 ) {
else if ( castle->GetGarrisonStrength( hero ) > heroStrength / 2 ) {
value -= dangerousTaskPenalty / 2;
}
else {
Expand Down Expand Up @@ -2079,52 +2079,75 @@ namespace AI
std::vector<HeroToMove> availableHeroes;

for ( Heroes * hero : heroes ) {
assert( hero != nullptr );

addHeroToMove( hero, availableHeroes );
}

const double originalMonsterStrengthMultiplier = _pathfinder.getCurrentArmyStrengthMultiplier();

const int monsterStrengthMultiplierCount = 2;
const double monsterStrengthMultipliers[monsterStrengthMultiplierCount] = { ARMY_ADVANTAGE_MEDIUM, ARMY_ADVANTAGE_SMALL };

Interface::StatusWindow & status = Interface::AdventureMap::Get().getStatusWindow();

uint32_t currentProgressValue = startProgressValue;

while ( !availableHeroes.empty() ) {
class AIWorldPathfinderStateRestorer
{
public:
AIWorldPathfinderStateRestorer( AIWorldPathfinder & pathfinder )
: _pathfinder( pathfinder )
, _originalMinimalArmyStrengthAdvantage( _pathfinder.getMinimalArmyStrengthAdvantage() )
, _originalSpellPointsReserveRatio( _pathfinder.getSpellPointsReserveRatio() )
{}

AIWorldPathfinderStateRestorer( const AIWorldPathfinderStateRestorer & ) = delete;

~AIWorldPathfinderStateRestorer()
{
_pathfinder.setMinimalArmyStrengthAdvantage( _originalMinimalArmyStrengthAdvantage );
_pathfinder.setSpellPointsReserveRatio( _originalSpellPointsReserveRatio );
}

AIWorldPathfinderStateRestorer & operator=( const AIWorldPathfinderStateRestorer & ) = delete;

private:
AIWorldPathfinder & _pathfinder;

const double _originalMinimalArmyStrengthAdvantage;
const double _originalSpellPointsReserveRatio;
};

const AIWorldPathfinderStateRestorer pathfinderStateRestorer( _pathfinder );

Heroes * bestHero = availableHeroes.front().hero;
double maxPriority = 0;
int bestTargetIndex = -1;

while ( true ) {
for ( const HeroToMove & heroInfo : availableHeroes ) {
double priority = -1;
const int targetIndex = getPriorityTarget( heroInfo, priority );
if ( targetIndex != -1 && ( priority > maxPriority || bestTargetIndex == -1 ) ) {
maxPriority = priority;
bestTargetIndex = targetIndex;
bestHero = heroInfo.hero;
}
}
{
const bool isLosingGame = bestHero->isLosingGame();

if ( bestTargetIndex != -1 ) {
break;
}
static const std::vector<std::pair<double, double>> commonPathfinderConfigurations{ { ARMY_ADVANTAGE_LARGE, 0.5 },
{ ARMY_ADVANTAGE_MEDIUM, 0.25 },
{ ARMY_ADVANTAGE_SMALL, 0.0 } };
static const std::vector<std::pair<double, double>> emergencyPathfinderConfigurations{ { ARMY_ADVANTAGE_DESPERATE, 0.0 } };

// If nowhere to move perhaps it's because of high monster estimation. Let's reduce it.
const double currentMonsterStrengthMultiplier = _pathfinder.getCurrentArmyStrengthMultiplier();
bool setNewMultiplier = false;
for ( int i = 0; i < monsterStrengthMultiplierCount; ++i ) {
if ( currentMonsterStrengthMultiplier > monsterStrengthMultipliers[i] ) {
_pathfinder.setArmyStrengthMultiplier( bestHero->isLosingGame() ? ARMY_ADVANTAGE_DESPERATE : monsterStrengthMultipliers[i] );
_pathfinder.setSpellPointReserve( 0 );
setNewMultiplier = true;
break;
for ( const auto & [minStrengthAdvantage, spReserveRatio] : isLosingGame ? emergencyPathfinderConfigurations : commonPathfinderConfigurations ) {
_pathfinder.setMinimalArmyStrengthAdvantage( minStrengthAdvantage );
_pathfinder.setSpellPointsReserveRatio( spReserveRatio );

double maxPriority = 0;

for ( const HeroToMove & heroInfo : availableHeroes ) {
double priority = -1;
const int targetIndex = getPriorityTarget( heroInfo, priority );

if ( targetIndex != -1 && ( priority > maxPriority || bestTargetIndex == -1 ) ) {
maxPriority = priority;
bestTargetIndex = targetIndex;
bestHero = heroInfo.hero;
}
}
}

if ( !setNewMultiplier ) {
break;
if ( bestTargetIndex != -1 ) {
break;
}
}
}

Expand Down Expand Up @@ -2152,12 +2175,11 @@ namespace AI
break;
}
}
}

if ( bestTargetIndex == -1 ) {
// Nothing to do. Stop everything
_pathfinder.setArmyStrengthMultiplier( originalMonsterStrengthMultiplier );
break;
}
if ( bestTargetIndex == -1 ) {
// Nothing to do. Stop everything
break;
}

const size_t heroesBefore = heroes.size();
Expand Down Expand Up @@ -2217,17 +2239,9 @@ namespace AI
addHeroToMove( heroes.back(), availableHeroes );
}

for ( size_t i = 0; i < availableHeroes.size(); ) {
if ( !availableHeroes[i].hero->MayStillMove( false, false ) ) {
availableHeroes.erase( availableHeroes.begin() + i );
continue;
}

++i;
}

_pathfinder.setArmyStrengthMultiplier( originalMonsterStrengthMultiplier );
_pathfinder.setSpellPointReserve( 0.5 );
availableHeroes.erase( std::remove_if( availableHeroes.begin(), availableHeroes.end(),
[]( const HeroToMove & item ) { return !item.hero->MayStillMove( false, false ); } ),
availableHeroes.end() );

// The size of heroes can be increased if a new hero is released from Jail.
const size_t maxHeroCount = std::max( heroes.size(), availableHeroes.size() );
Expand All @@ -2242,13 +2256,8 @@ namespace AI
}
}

const bool allHeroesMoved = availableHeroes.empty();

_pathfinder.setArmyStrengthMultiplier( originalMonsterStrengthMultiplier );
_pathfinder.setSpellPointReserve( 0.5 );

status.DrawAITurnProgress( endProgressValue );

return allHeroesMoved;
return availableHeroes.empty();
}
}
13 changes: 7 additions & 6 deletions src/fheroes2/castle/castle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/

#include "castle.h"

#include <algorithm>
#include <array>
#include <cassert>
Expand All @@ -34,7 +36,6 @@
#include "audio_manager.h"
#include "battle_board.h"
#include "battle_tower.h"
#include "castle.h"
#include "castle_building_info.h"
#include "dialog.h"
#include "difficulty.h"
Expand Down Expand Up @@ -2434,7 +2435,7 @@ Army & Castle::GetActualArmy()
return hero ? hero->GetArmy() : army;
}

double Castle::GetGarrisonStrength( const Heroes * attackingHero ) const
double Castle::GetGarrisonStrength( const Heroes & attackingHero ) const
{
double totalStrength = 0;

Expand Down Expand Up @@ -2470,13 +2471,13 @@ double Castle::GetGarrisonStrength( const Heroes * attackingHero ) const
totalStrength += towerStr / 2;
}

if ( attackingHero && ( !attackingHero->GetArmy().isMeleeDominantArmy() || attackingHero->HasSecondarySkill( Skill::Secondary::BALLISTICS ) ) ) {
totalStrength *= isBuild( BUILD_MOAT ) ? 1.2 : 1.15;
}
else {
if ( !attackingHero.HasSecondarySkill( Skill::Secondary::BALLISTICS ) && attackingHero.GetArmy().isMeleeDominantArmy() ) {
// Heavy penalty if the attacking hero does not have a ballistic skill, and his army is based on melee infantry
totalStrength *= isBuild( BUILD_MOAT ) ? 1.45 : 1.25;
}
else {
totalStrength *= isBuild( BUILD_MOAT ) ? 1.2 : 1.15;
}
}

return totalStrength;
Expand Down
2 changes: 1 addition & 1 deletion src/fheroes2/castle/castle.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class Castle : public MapPosition, public BitModes, public ColorBase, public Con
// castle - including an estimate of the strength of the combined army consisting of the garrison and
// the hero's troops (if present), castle-specific bonuses from moat, towers and so on, relative to
// the attacking hero's abilities. See the implementation for details.
double GetGarrisonStrength( const Heroes * attackingHero ) const;
double GetGarrisonStrength( const Heroes & attackingHero ) const;

// Returns the correct dwelling type available in the castle. BUILD_NOTHING is returned if this is not a dwelling.
uint32_t GetActualDwelling( const uint32_t buildId ) const;
Expand Down
2 changes: 1 addition & 1 deletion src/fheroes2/heroes/heroes_base.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ bool HeroBase::CanCastSpell( const Spell & spell, std::string * res /* = nullptr
bool hasCastles = std::any_of( castles.begin(), castles.end(), []( const Castle * castle ) { return castle && castle->GetHero() == nullptr; } );
if ( !hasCastles ) {
if ( res != nullptr ) {
*res = _( "You do not currently own any town or castle, so you can't cast the spell." );
*res = _( "You do not currently own any town or castle that is not occupied by a hero, so you can't cast the spell." );
}
return false;
}
Expand Down
4 changes: 2 additions & 2 deletions src/fheroes2/heroes/heroes_spell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ namespace

void HeroesTownGate( Heroes & hero, const Castle * castle )
{
assert( castle != nullptr );
assert( castle && castle->GetHero() == nullptr );

Interface::AdventureMap & I = Interface::AdventureMap::Get();

Expand Down Expand Up @@ -380,7 +380,7 @@ namespace
return false;
}

if ( castle->GetHero() && castle->GetHero() != &hero ) {
if ( castle->GetHero() ) {
// The nearest town occupation must be checked before casting this spell. Something is wrong with the logic!
assert( 0 );
return false;
Expand Down
Loading

0 comments on commit 16ae0d9

Please sign in to comment.