Skip to content

Commit

Permalink
Make rules for AI surrender and retreat more unified (#8297)
Browse files Browse the repository at this point in the history
  • Loading branch information
oleg-derevenetz authored Jan 25, 2024
1 parent 8fe65b7 commit 3bfc40c
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 83 deletions.
2 changes: 0 additions & 2 deletions src/fheroes2/ai/ai.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,6 @@ namespace AI
// Transfers the slowest troops from the hero's army to the garrison to try to get a movement bonus on the next turn
void transferSlowestTroopsToGarrison( Heroes * hero, Castle * castle );

bool CanPurchaseHero( const Kingdom & kingdom );

// Calculates a marketplace transaction, after which the kingdom would be able to make a payment in the amount of
// at least 'fundsToObtain'. Returns the corresponding transaction if it was found, otherwise returns an empty
// result. In order to receive the necessary funds, the returned transaction must be deducted from the funds of
Expand Down
16 changes: 0 additions & 16 deletions src/fheroes2/ai/ai_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
#include "payment.h"
#include "resource.h"
#include "resource_trading.h"
#include "settings.h"

namespace AI
{
Expand Down Expand Up @@ -225,21 +224,6 @@ namespace AI
assert( army.isValid() );
}

bool CanPurchaseHero( const Kingdom & kingdom )
{
if ( kingdom.GetCountCastle() == 0 ) {
return false;
}

if ( kingdom.GetColor() == Settings::Get().CurrentColor() ) {
// This is the AI's current turn.
return kingdom.AllowPayment( PaymentConditions::RecruitHero() );
}

// This is not the current turn for the AI so we need to roughly calculate the possible future income on the next day.
return kingdom.AllowPayment( PaymentConditions::RecruitHero() - kingdom.GetIncome() );
}

std::optional<Funds> calculateMarketplaceTransaction( const Kingdom & kingdom, const Funds & fundsToObtain )
{
const uint32_t marketplaceCount = kingdom.GetCountMarketplace();
Expand Down
3 changes: 0 additions & 3 deletions src/fheroes2/ai/normal/ai_normal.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,8 @@ namespace AI

bool isPositionLocatedInDefendedArea( const Battle::Unit & currentUnit, const Battle::Position & pos ) const;
bool isUnitFaster( const Battle::Unit & currentUnit, const Battle::Unit & target ) const;
bool isHeroWorthSaving( const Heroes & hero ) const;
bool isCommanderCanSpellcast( const Battle::Arena & arena, const HeroBase * commander ) const;

bool checkRetreatCondition( const Heroes & hero ) const;

static double commanderMaximumSpellDamageValue( const HeroBase & commander );

// When this limit of turns without deaths is exceeded for an attacking AI-controlled hero,
Expand Down
158 changes: 116 additions & 42 deletions src/fheroes2/ai/normal/ai_normal_battle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -484,28 +484,12 @@ namespace AI
}
}

bool BattlePlanner::isHeroWorthSaving( const Heroes & hero ) const
{
return hero.GetLevel() > 2 || !hero.GetBagArtifacts().empty();
}

bool BattlePlanner::isCommanderCanSpellcast( const Arena & arena, const HeroBase * commander ) const
{
return commander && ( !commander->isControlHuman() || Settings::Get().BattleAutoSpellcast() ) && commander->HaveSpellBook()
&& !commander->Modes( Heroes::SPELLCASTED ) && !arena.isSpellcastDisabled();
}

bool BattlePlanner::checkRetreatCondition( const Heroes & hero ) const
{
if ( !_considerRetreat || hero.isControlHuman() || hero.isLosingGame() || !isHeroWorthSaving( hero ) || !CanPurchaseHero( hero.GetKingdom() ) ) {
return false;
}

// Retreat if remaining army strength is a fraction of enemy's
// Consider taking speed/turn order into account in the future
return _myArmyStrength * Difficulty::getArmyStrengthRatioForAIRetreat( Game::getDifficulty() ) < _enemyArmyStrength;
}

bool BattlePlanner::isUnitFaster( const Unit & currentUnit, const Unit & target ) const
{
if ( currentUnit.isFlying() == target.isFlying() )
Expand Down Expand Up @@ -594,24 +578,7 @@ namespace AI

// Step 2. Check retreat/surrender condition
const Heroes * actualHero = dynamic_cast<const Heroes *>( _commander );
if ( actualHero && checkRetreatCondition( *actualHero ) ) {
const auto farewellSpellcast = [this, &arena, &currentUnit, &actions]() {
if ( !isCommanderCanSpellcast( arena, _commander ) ) {
return;
}

// Cast a spell with maximum damage
const SpellSelection & bestSpell = selectBestSpell( arena, currentUnit, true );
if ( bestSpell.spellID == -1 ) {
return;
}

actions.emplace_back( Command::SPELLCAST, bestSpell.spellID, bestSpell.cell );

DEBUG_LOG( DBG_BATTLE, DBG_INFO,
arena.GetCurrentCommander()->GetName() << " casts " << Spell( bestSpell.spellID ).GetName() << " on cell " << bestSpell.cell )
};

if ( actualHero ) {
enum class Outcome
{
ContinueBattle,
Expand All @@ -620,14 +587,105 @@ namespace AI
};

const Outcome outcome = [this, &arena, actualHero]() {
const Force & force = arena.getForce( _myColor );
if ( !_considerRetreat ) {
return Outcome::ContinueBattle;
}

// Human-controlled heroes should not retreat or surrender during auto/instant battles
if ( actualHero->isControlHuman() ) {
return Outcome::ContinueBattle;
}

const int gameDifficulty = Game::getDifficulty();
const bool isGameCampaign = Game::isCampaign();

// TODO: consider taking speed/turn order into account in the future
if ( _myArmyStrength * Difficulty::getArmyStrengthRatioForAIRetreat( gameDifficulty ) >= _enemyArmyStrength ) {
return Outcome::ContinueBattle;
}

const bool hasValuableArtifacts = [actualHero]() {
const BagArtifacts & artifactsBag = actualHero->GetBagArtifacts();

return std::any_of( artifactsBag.begin(), artifactsBag.end(), []( const Artifact & art ) {
const fheroes2::ArtifactData & artifactData = fheroes2::getArtifactData( art.GetID() );

return std::any_of( artifactData.bonuses.begin(), artifactData.bonuses.end(),
[]( const fheroes2::ArtifactBonus & bonus ) { return bonus.type != fheroes2::ArtifactBonusType::NONE; } );
} );
}();

const Kingdom & kingdom = actualHero->GetKingdom();

const bool canRetreat = arena.CanRetreatOpponent( _myColor );
const bool canSurrender = arena.CanSurrenderOpponent( _myColor );
const bool considerRetreat = [this, &arena, actualHero, gameDifficulty, isGameCampaign, hasValuableArtifacts, &kingdom]() {
if ( !Difficulty::allowAIToRetreat( gameDifficulty, isGameCampaign ) ) {
return false;
}

if ( !canRetreat ) {
if ( !canSurrender ) {
if ( !arena.CanRetreatOpponent( _myColor ) ) {
return false;
}

// If the hero has valuable artifacts, he should in any case consider retreating so that these artifacts do not end up at the disposal of the enemy,
// especially in the case of an alliance war
if ( hasValuableArtifacts ) {
return true;
}

// Otherwise, if this hero is the last one, and the kingdom has no castles, then there is no point in retreating
if ( kingdom.GetHeroes().size() == 1 ) {
assert( kingdom.GetHeroes().at( 0 ) == actualHero );

if ( kingdom.GetCastles().empty() ) {
return false;
}
}

// Otherwise, if this hero is relatively experienced, then he should think about retreating so that he can be hired again later
return actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty );
}();

const bool considerSurrender = [this, &arena, actualHero, gameDifficulty, isGameCampaign, hasValuableArtifacts, &kingdom]() {
if ( !Difficulty::allowAIToSurrender( gameDifficulty, isGameCampaign ) ) {
return false;
}

if ( !arena.CanSurrenderOpponent( _myColor ) ) {
return false;
}

// If the hero has valuable artifacts, he should in any case consider surrendering so that these artifacts do not end up at the disposal of the enemy,
// especially in the case of an alliance war
if ( hasValuableArtifacts ) {
return true;
}

// Otherwise, if this hero is the last one, and either the kingdom has no castles, or this hero is defending the last castle, then there is no point
// in surrendering
if ( kingdom.GetHeroes().size() == 1 ) {
assert( kingdom.GetHeroes().at( 0 ) == actualHero );

const VecCastles & castles = kingdom.GetCastles();
if ( castles.empty() ) {
return false;
}

const Castle * castle = actualHero->inCastle();
if ( castle && castles.size() == 1 ) {
assert( castles.at( 0 ) == castle );

return false;
}
}

// Otherwise, if this hero is relatively experienced, then he should think about surrendering so that he can be hired again later
return actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty );
}();

const Force & force = arena.getForce( _myColor );

if ( !considerRetreat ) {
if ( !considerSurrender ) {
return Outcome::ContinueBattle;
}

Expand All @@ -638,22 +696,38 @@ namespace AI
return Outcome::Surrender;
}

if ( !canSurrender ) {
if ( !considerSurrender ) {
return Outcome::Retreat;
}

if ( force.getStrengthOfArmyRemainingInCaseOfSurrender() < Army::getStrengthOfAverageStartingArmy( actualHero ) ) {
return Outcome::Retreat;
}

if ( !kingdom.AllowPayment( Funds{ Resource::GOLD, force.GetSurrenderCost() }
* Difficulty::getGoldReserveRatioForAISurrender( Game::getDifficulty() ) ) ) {
if ( !kingdom.AllowPayment( Funds{ Resource::GOLD, force.GetSurrenderCost() } * Difficulty::getGoldReserveRatioForAISurrender( gameDifficulty ) ) ) {
return Outcome::Retreat;
}

return Outcome::Surrender;
}();

const auto farewellSpellcast = [this, &arena, &currentUnit, &actions]() {
if ( !isCommanderCanSpellcast( arena, _commander ) ) {
return;
}

// Cast a spell with maximum damage
const SpellSelection & bestSpell = selectBestSpell( arena, currentUnit, true );
if ( bestSpell.spellID == -1 ) {
return;
}

actions.emplace_back( Command::SPELLCAST, bestSpell.spellID, bestSpell.cell );

DEBUG_LOG( DBG_BATTLE, DBG_INFO,
arena.GetCurrentCommander()->GetName() << " casts " << Spell( bestSpell.spellID ).GetName() << " on cell " << bestSpell.cell )
};

switch ( outcome ) {
case Outcome::ContinueBattle:
break;
Expand Down
8 changes: 4 additions & 4 deletions src/fheroes2/ai/normal/ai_normal_hero.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1014,8 +1014,8 @@ namespace AI
}

if ( hero.getAIRole() == otherHero->getAIRole()
&& hero.getStatsValue() + Difficulty::getMinStatDiffBetweenAIRoles( Game::getDifficulty() ) > otherHero->getStatsValue() ) {
// Two heroes are almost identical. No reason to meet.
&& hero.getStatsValue() + Difficulty::getMinStatDiffForAIHeroesMeeting( Game::getDifficulty() ) > otherHero->getStatsValue() ) {
// Either this hero is superior or approximately equal to another hero in terms of stats. Do not waste time for meeting. Let him come instead.
return valueToIgnore;
}

Expand Down Expand Up @@ -1643,8 +1643,8 @@ namespace AI
}

if ( hero.getAIRole() == otherHero->getAIRole()
&& hero.getStatsValue() + Difficulty::getMinStatDiffBetweenAIRoles( Game::getDifficulty() ) + 1 > otherHero->getStatsValue() ) {
// Two heroes are almost identical. No reason to meet.
&& hero.getStatsValue() + Difficulty::getMinStatDiffForAIHeroesMeeting( Game::getDifficulty() ) + 1 > otherHero->getStatsValue() ) {
// Either this hero is superior or approximately equal to another hero in terms of stats. Do not waste time for meeting. Let him come instead.
return valueToIgnore;
}

Expand Down
3 changes: 1 addition & 2 deletions src/fheroes2/battle/battle_arena.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
#include "heroes.h"
#include "heroes_base.h"
#include "icn.h"
#include "kingdom.h"
#include "localevent.h"
#include "logging.h"
#include "maps.h"
Expand Down Expand Up @@ -818,7 +817,7 @@ bool Battle::Arena::CanSurrenderOpponent( int color ) const
{
const HeroBase * hero = getCommander( color );
const HeroBase * enemyHero = getEnemyCommander( color );
return hero && hero->isHeroes() && enemyHero && ( enemyHero->isHeroes() || enemyHero->isCaptain() ) && !world.GetKingdom( hero->GetColor() ).GetCastles().empty();
return hero && hero->isHeroes() && enemyHero && ( enemyHero->isHeroes() || enemyHero->isCaptain() );
}

bool Battle::Arena::CanRetreatOpponent( int color ) const
Expand Down
25 changes: 20 additions & 5 deletions src/fheroes2/game/difficulty.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ std::string Difficulty::String( int difficulty )
return "Unknown";
}

int Difficulty::GetScoutingBonus( int difficulty )
int Difficulty::GetScoutingBonusForAI( int difficulty )
{
switch ( difficulty ) {
case Difficulty::NORMAL:
Expand Down Expand Up @@ -116,7 +116,7 @@ double Difficulty::GetUnitGrowthBonusForAI( const int difficulty, const bool /*
return 0;
}

int Difficulty::GetHeroMovementBonus( int difficulty )
int Difficulty::GetHeroMovementBonusForAI( int difficulty )
{
switch ( difficulty ) {
case Difficulty::EXPERT:
Expand All @@ -128,12 +128,27 @@ int Difficulty::GetHeroMovementBonus( int difficulty )
return 0;
}

bool Difficulty::allowAIToRetreat( const int /* difficulty */, const bool /* isCampaign */ )
{
return true;
}

bool Difficulty::allowAIToSurrender( const int /* difficulty */, const bool /* isCampaign */ )
{
return true;
}

int Difficulty::getMinHeroLevelForAIRetreat( const int /* difficulty */ )
{
return 3;
}

double Difficulty::getArmyStrengthRatioForAIRetreat( const int difficulty )
{
switch ( difficulty ) {
case Difficulty::NORMAL:
return 100.0 / 7.5;
case Difficulty::HARD: // fall-through
case Difficulty::HARD:
case Difficulty::EXPERT:
return 100.0 / 8.5;
case Difficulty::IMPOSSIBLE:
Expand All @@ -149,7 +164,7 @@ uint32_t Difficulty::getGoldReserveRatioForAISurrender( const int /* difficulty
return 10;
}

uint32_t Difficulty::GetDimensionDoorLimit( int difficulty )
uint32_t Difficulty::GetDimensionDoorLimitForAI( int difficulty )
{
switch ( difficulty ) {
case Difficulty::EASY:
Expand Down Expand Up @@ -183,7 +198,7 @@ bool Difficulty::areAIHeroRolesAllowed( const int difficulty )
return true;
}

int Difficulty::getMinStatDiffBetweenAIRoles( const int difficulty )
int Difficulty::getMinStatDiffForAIHeroesMeeting( const int difficulty )
{
switch ( difficulty ) {
case Difficulty::EASY:
Expand Down
Loading

0 comments on commit 3bfc40c

Please sign in to comment.