From 866dcb690f0ceabafe34e44eea3c18317f6ea5ad Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Tue, 13 Feb 2024 21:12:54 +0300 Subject: [PATCH 01/40] Remove the AI unit growth bonuses and income bonuses for all difficulty levels --- src/fheroes2/game/difficulty.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index a786883f660..0b22574c0f5 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -64,8 +64,9 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) return 0; } -double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) +double Difficulty::getGoldIncomeBonusForAI( const int /* difficulty */ ) { + /* switch ( difficulty ) { case Difficulty::EASY: // It is deduction from the income. @@ -79,10 +80,11 @@ double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) default: break; } + */ return 0; } -double Difficulty::GetUnitGrowthBonusForAI( const int difficulty, const bool /* isCampaign */, const building_t /* dwelling */ ) +double Difficulty::GetUnitGrowthBonusForAI( const int /* difficulty */, const bool /* isCampaign */, const building_t /* dwelling */ ) { // In the original game AI has a cheeky monster growth bonus depending on difficulty: // Easy - 0.0 (no bonus) @@ -98,6 +100,7 @@ double Difficulty::GetUnitGrowthBonusForAI( const int difficulty, const bool /* // Completely removing these bonuses might break some maps and they become unplayable. // Therefore, these bonuses are reduced by 5% which is the value of noise in many processes / systems. + /* switch ( difficulty ) { case Difficulty::EASY: case Difficulty::NORMAL: @@ -113,6 +116,7 @@ double Difficulty::GetUnitGrowthBonusForAI( const int difficulty, const bool /* assert( 0 ); break; } + */ return 0; } From cbc4c133fd003532f67dbbfe60fc47cb23414484 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 15 Feb 2024 19:25:50 +0300 Subject: [PATCH 02/40] Tune the AI gold bonus --- src/fheroes2/game/difficulty.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 0b22574c0f5..8993f1961ae 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -64,23 +64,21 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) return 0; } -double Difficulty::getGoldIncomeBonusForAI( const int /* difficulty */ ) +double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) { - /* switch ( difficulty ) { case Difficulty::EASY: // It is deduction from the income. return -0.25; case Difficulty::HARD: - return 0.29; + return 0.5; case Difficulty::EXPERT: - return 0.45; + return 1.0; case Difficulty::IMPOSSIBLE: - return 0.6; + return 2.0; default: break; } - */ return 0; } From 04be0fe2e6fcd105ea92e2bbb189062a7de3e1af Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sun, 18 Feb 2024 20:54:59 +0300 Subject: [PATCH 03/40] Tune the unit growth bonus --- src/fheroes2/game/difficulty.cpp | 38 ++++++++++---------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 8993f1961ae..35e35ab5a15 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -61,6 +61,7 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) default: break; } + return 0; } @@ -79,42 +80,21 @@ double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) default: break; } + return 0; } -double Difficulty::GetUnitGrowthBonusForAI( const int /* difficulty */, const bool /* isCampaign */, const building_t /* dwelling */ ) +double Difficulty::GetUnitGrowthBonusForAI( const int difficulty, const bool /* isCampaign */, const building_t /* dwelling */ ) { - // In the original game AI has a cheeky monster growth bonus depending on difficulty: - // Easy - 0.0 (no bonus) - // Normal - 0.0 (no bonus) - // Hard - 0.20 (or 20% extra) - // Expert - 0.32 (or 32% extra) - // Impossible - 0.44 (or 44% extra) - // This bonus was introduced to compensate weak AI in the game. - // - // However, with introduction of proper AI in this engine AI has become much stronger and some maps are impossible to beat. - // Also this bonus can be abused by players while capturing AI castles on a first day of a week. - // - // Completely removing these bonuses might break some maps and they become unplayable. - // Therefore, these bonuses are reduced by 5% which is the value of noise in many processes / systems. - - /* switch ( difficulty ) { - case Difficulty::EASY: - case Difficulty::NORMAL: - return 0; - case Difficulty::HARD: - return 0.14; case Difficulty::EXPERT: - return 0.254; + return 0.25; case Difficulty::IMPOSSIBLE: - return 0.368; + return 0.33; default: - // Did you add a new difficulty level? Add the logic above! - assert( 0 ); break; } - */ + return 0; } @@ -127,6 +107,7 @@ int Difficulty::GetHeroMovementBonusForAI( int difficulty ) default: break; } + return 0; } @@ -158,6 +139,7 @@ double Difficulty::getArmyStrengthRatioForAIRetreat( const int difficulty ) default: break; } + return 100.0 / 6.0; } @@ -178,6 +160,7 @@ uint32_t Difficulty::GetDimensionDoorLimitForAI( int difficulty ) default: break; } + return UINT32_MAX; } @@ -229,6 +212,7 @@ bool Difficulty::allowAIToSplitWeakStacks( const int difficulty ) default: break; } + return true; } @@ -240,6 +224,7 @@ bool Difficulty::allowAIToDevelopCastlesOnDay( const int difficulty, const bool default: break; } + return true; } @@ -252,5 +237,6 @@ bool Difficulty::allowAIToBuildCastleBuilding( const int difficulty, const bool default: break; } + return true; } From d098ba3c0198294a7211aa685eff38a674824a6f Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Wed, 21 Feb 2024 20:44:55 +0300 Subject: [PATCH 04/40] Remove the unit growth bonuses once again --- src/fheroes2/game/difficulty.cpp | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 35e35ab5a15..a50b4a4f1e2 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -84,17 +84,8 @@ double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) return 0; } -double Difficulty::GetUnitGrowthBonusForAI( const int difficulty, const bool /* isCampaign */, const building_t /* dwelling */ ) +double Difficulty::GetUnitGrowthBonusForAI( const int /* difficulty */, const bool /* isCampaign */, const building_t /* dwelling */ ) { - switch ( difficulty ) { - case Difficulty::EXPERT: - return 0.25; - case Difficulty::IMPOSSIBLE: - return 0.33; - default: - break; - } - return 0; } From 01aed5df59606fe7ab48c144ed6ae1a3d042f96b Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 22 Feb 2024 00:01:56 +0300 Subject: [PATCH 05/40] Add the infrastructure to apply the AI resource bonuses --- src/fheroes2/game/difficulty.cpp | 14 ++++++++++++++ src/fheroes2/game/difficulty.h | 5 ++++- src/fheroes2/kingdom/kingdom.cpp | 25 ++++++++++++++++++++----- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index a50b4a4f1e2..f509d3b29c5 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -65,6 +65,20 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) return 0; } +Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty ) +{ + switch ( difficulty ) { + case Difficulty::EXPERT: + return { 2, 2, 1, 1, 1, 1, 0 }; + case Difficulty::IMPOSSIBLE: + return { 2, 2, 1, 1, 1, 1, 0 }; + default: + break; + } + + return {}; +} + double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) { switch ( difficulty ) { diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index c6ec0dee3a9..df3baaa52d5 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -45,7 +45,10 @@ namespace Difficulty int GetScoutingBonusForAI( int difficulty ); - // Returns an extra gold bonus modifier for AI based on difficulty level. + // Returns an extra resource bonus for AI based on difficulty level. + Funds getResourceIncomeBonusForAI( const int difficulty ); + + // Returns an extra gold bonus modifier for AI based on difficulty level. This modifier is applied after applying the resource income bonus. double getGoldIncomeBonusForAI( const int difficulty ); // Returns an extra growth bonus modifier for AI based on difficulty level. diff --git a/src/fheroes2/kingdom/kingdom.cpp b/src/fheroes2/kingdom/kingdom.cpp index f91057af782..3a7f1d4319b 100644 --- a/src/fheroes2/kingdom/kingdom.cpp +++ b/src/fheroes2/kingdom/kingdom.cpp @@ -85,7 +85,9 @@ namespace Funds getHandicapDependentIncome( const Funds & original, const Player::HandicapStatus handicapStatus ) { const int32_t handicapPercentage = getHandicapIncomePercentage( handicapStatus ); + Funds corrected( original ); + corrected.wood = std::min( corrected.wood, ( corrected.wood * handicapPercentage + 99 ) / 100 ); corrected.mercury = std::min( corrected.mercury, ( corrected.mercury * handicapPercentage + 99 ) / 100 ); corrected.ore = std::min( corrected.ore, ( corrected.ore * handicapPercentage + 99 ) / 100 ); @@ -660,7 +662,7 @@ uint32_t Kingdom::GetMaxHeroes() return GameStatic::GetKingdomMaxHeroes(); } -Funds Kingdom::GetIncome( int type /* INCOME_ALL */ ) const +Funds Kingdom::GetIncome( int type /* = INCOME_ALL */ ) const { Funds totalIncome; @@ -719,15 +721,28 @@ Funds Kingdom::GetIncome( int type /* INCOME_ALL */ ) const } } - if ( isControlAI() && totalIncome.gold > 0 ) { - const int32_t bonusGold = static_cast( totalIncome.gold * Difficulty::getGoldIncomeBonusForAI( Game::getDifficulty() ) ); + if ( isControlAI() ) { + const Funds incomeBonus = Difficulty::getResourceIncomeBonusForAI( Game::getDifficulty() ); + if ( incomeBonus.GetValidItemsCount() != 0 ) { + DEBUG_LOG( DBG_AI, DBG_TRACE, "AI bonus to the resource income has been applied to " << Color::String( color ) << ": " << incomeBonus.String() ); + + totalIncome += incomeBonus; + } + + const int32_t goldBonus = static_cast( totalIncome.gold * Difficulty::getGoldIncomeBonusForAI( Game::getDifficulty() ) ); + if ( goldBonus != 0 ) { + DEBUG_LOG( DBG_AI, DBG_TRACE, + "AI bonus to the gold income has been applied to " << Color::String( color ) << ", original income: " << totalIncome.gold + << ", bonus income: " << goldBonus ); - totalIncome.gold += bonusGold; + totalIncome.gold += goldBonus; + } } - // Some human players can have handicap for resources. const Player * player = Players::Get( color ); assert( player != nullptr ); + + // Some human players can have handicap for resources. return getHandicapDependentIncome( totalIncome, player->getHandicapStatus() ); } From 9dfdde7cb16325e25069b934443337f60ff2e562 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 22 Feb 2024 00:23:46 +0300 Subject: [PATCH 06/40] Address the Clang-Tidy warning --- src/fheroes2/game/difficulty.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index f509d3b29c5..8c36f2499f4 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -69,7 +69,6 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty ) { switch ( difficulty ) { case Difficulty::EXPERT: - return { 2, 2, 1, 1, 1, 1, 0 }; case Difficulty::IMPOSSIBLE: return { 2, 2, 1, 1, 1, 1, 0 }; default: From 4b00fcb8168575afc50a3101a414e65b49f74c7c Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 22 Feb 2024 00:26:12 +0300 Subject: [PATCH 07/40] Apply the IWYU suggestions --- src/fheroes2/game/difficulty.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index df3baaa52d5..117efeec574 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -27,6 +27,7 @@ #include #include "castle.h" +#include "resource.h" namespace Difficulty { From 5249aecdc556981228444eda255ea16186baf660 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 22 Feb 2024 00:35:40 +0300 Subject: [PATCH 08/40] Implement the infrastructure for per-race growth bonuses --- src/fheroes2/castle/castle.cpp | 2 +- src/fheroes2/game/difficulty.cpp | 2 +- src/fheroes2/game/difficulty.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fheroes2/castle/castle.cpp b/src/fheroes2/castle/castle.cpp index d1c4893fd42..5bf9a32aa29 100644 --- a/src/fheroes2/castle/castle.cpp +++ b/src/fheroes2/castle/castle.cpp @@ -942,7 +942,7 @@ void Castle::ActionNewWeekAIBonuses() continue; } - const long bonusGrowth = std::lround( originalGrowth * Difficulty::GetUnitGrowthBonusForAI( Game::getDifficulty(), Game::isCampaign(), dwellingId ) ); + const long bonusGrowth = std::lround( originalGrowth * Difficulty::GetUnitGrowthBonusForAI( Game::getDifficulty(), Game::isCampaign(), race, dwellingId ) ); if ( bonusGrowth >= 0 ) { *dwellingMonsters += bonusGrowth; diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 8c36f2499f4..bcbc23853fe 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -97,7 +97,7 @@ double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) return 0; } -double Difficulty::GetUnitGrowthBonusForAI( const int /* difficulty */, const bool /* isCampaign */, const building_t /* dwelling */ ) +double Difficulty::GetUnitGrowthBonusForAI( const int /* difficulty */, const bool /* isCampaign */, const int /* race */, const building_t /* dwelling */ ) { return 0; } diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index 117efeec574..50c8ecd1f04 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -53,7 +53,7 @@ namespace Difficulty double getGoldIncomeBonusForAI( const int difficulty ); // Returns an extra growth bonus modifier for AI based on difficulty level. - double GetUnitGrowthBonusForAI( const int difficulty, const bool isCampaign, const building_t dwelling ); + double GetUnitGrowthBonusForAI( const int difficulty, const bool isCampaign, const int race, const building_t dwelling ); int GetHeroMovementBonusForAI( int difficulty ); From 7d69ce75bca70ad13418824b726f17f6d95adda9 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 22 Feb 2024 00:41:07 +0300 Subject: [PATCH 09/40] Disable the level 5 mage guilds on Easy difficulty --- src/fheroes2/game/difficulty.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index bcbc23853fe..a090173067d 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -237,7 +237,7 @@ bool Difficulty::allowAIToBuildCastleBuilding( const int difficulty, const bool switch ( difficulty ) { case Difficulty::EASY: // Only the construction of the corresponding dwelling is limited, but not its upgrade - return isCampaign || building != DWELLING_MONSTER6; + return isCampaign || ( building != DWELLING_MONSTER6 && building != BUILD_MAGEGUILD5 ); default: break; } From 93e7a5dd2833110677454b7f943f47af4c3dfe74 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 22 Feb 2024 00:50:35 +0300 Subject: [PATCH 10/40] Add comment --- src/fheroes2/game/difficulty.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index 50c8ecd1f04..3af4a7ef3f2 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -69,6 +69,8 @@ namespace Difficulty // Returns the minimum ratio of the AI kingdom's gold reserve to the cost of surrender, at which the AI will prefer surrender to retreat from the battlefield uint32_t getGoldReserveRatioForAISurrender( const int difficulty ); + // Returns the limit on the number of times the Dimension Door spell can be cast, which is applied to each of the AI-controlled heroes individually during one AI + // turn uint32_t GetDimensionDoorLimitForAI( int difficulty ); bool areAIHeroRolesAllowed( const int difficulty ); From 3da35bb7211ad98d9c55d40e90b1fe5fae88ab2e Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sun, 25 Feb 2024 22:53:36 +0300 Subject: [PATCH 11/40] Tune the resource income --- src/fheroes2/game/difficulty.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index a090173067d..0b0113ad90f 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -25,6 +25,7 @@ #include +#include "profit.h" #include "translations.h" std::string Difficulty::String( int difficulty ) @@ -67,10 +68,19 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty ) { + const auto getIncomeFromCertainNumberOfResourceMinesExceptGold = []( const uint32_t numOfMines ) { + Funds result; + + Resource::forEach( ( Resource::ALL & ~Resource::GOLD ), [&result]( const int res ) { result += ProfitConditions::FromMine( res ); } ); + + return result * numOfMines; + }; + switch ( difficulty ) { case Difficulty::EXPERT: + return getIncomeFromCertainNumberOfResourceMinesExceptGold( 1 ); case Difficulty::IMPOSSIBLE: - return { 2, 2, 1, 1, 1, 1, 0 }; + return getIncomeFromCertainNumberOfResourceMinesExceptGold( 2 ); default: break; } @@ -85,11 +95,11 @@ double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) // It is deduction from the income. return -0.25; case Difficulty::HARD: - return 0.5; - case Difficulty::EXPERT: return 1.0; - case Difficulty::IMPOSSIBLE: + case Difficulty::EXPERT: return 2.0; + case Difficulty::IMPOSSIBLE: + return 3.0; default: break; } From 3a3b55a995ae6d0697e6eda6198ab1cbbb561b2a Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Mon, 26 Feb 2024 01:22:57 +0300 Subject: [PATCH 12/40] Rename the lambda --- src/fheroes2/game/difficulty.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 0b0113ad90f..d0cda474f43 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -68,19 +68,19 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty ) { - const auto getIncomeFromCertainNumberOfResourceMinesExceptGold = []( const uint32_t numOfMines ) { + const auto getIncomeFromSetsOfResourceMines = []( const uint32_t numOfSets ) { Funds result; Resource::forEach( ( Resource::ALL & ~Resource::GOLD ), [&result]( const int res ) { result += ProfitConditions::FromMine( res ); } ); - return result * numOfMines; + return result * numOfSets; }; switch ( difficulty ) { case Difficulty::EXPERT: - return getIncomeFromCertainNumberOfResourceMinesExceptGold( 1 ); + return getIncomeFromSetsOfResourceMines( 1 ); case Difficulty::IMPOSSIBLE: - return getIncomeFromCertainNumberOfResourceMinesExceptGold( 2 ); + return getIncomeFromSetsOfResourceMines( 2 ); default: break; } From 4174a1f2fdb81f6445a37048b49e29c449d565d3 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 2 Mar 2024 01:15:49 +0300 Subject: [PATCH 13/40] Improve the defensive variant of the castle development a bit --- src/fheroes2/ai/ai.h | 5 ++- src/fheroes2/ai/normal/ai_normal.h | 47 ++++++++++---------- src/fheroes2/ai/normal/ai_normal_castle.cpp | 31 ++++++------- src/fheroes2/ai/normal/ai_normal_kingdom.cpp | 13 +++--- src/fheroes2/kingdom/kingdom.h | 1 + 5 files changed, 48 insertions(+), 49 deletions(-) diff --git a/src/fheroes2/ai/ai.h b/src/fheroes2/ai/ai.h index 3296fc90425..f2ae6ed52dc 100644 --- a/src/fheroes2/ai/ai.h +++ b/src/fheroes2/ai/ai.h @@ -115,6 +115,7 @@ namespace AI virtual void Reset(); virtual void resetPathfinder() = 0; + virtual bool isValidHeroObject( const Heroes & hero, const int32_t index, const bool underHero ) = 0; // Should be called at the beginning of the battle even if no AI-controlled players are @@ -124,10 +125,10 @@ namespace AI virtual void tradingPostVisitEvent( Kingdom & kingdom ) = 0; protected: - int _personality = NONE; - Base() = default; + int _personality = NONE; + private: friend StreamBase & operator<<( StreamBase &, const AI::Base & ); friend StreamBase & operator>>( StreamBase &, AI::Base & ); diff --git a/src/fheroes2/ai/normal/ai_normal.h b/src/fheroes2/ai/normal/ai_normal.h index af0a6b8334c..30da3288dc7 100644 --- a/src/fheroes2/ai/normal/ai_normal.h +++ b/src/fheroes2/ai/normal/ai_normal.h @@ -270,42 +270,30 @@ namespace AI void CastlePreBattle( Castle & castle ) override; - bool recruitHero( Castle & castle, bool buyArmy, bool underThreat ); - void reinforceHeroInCastle( Heroes & hero, Castle & castle, const Funds & budget ); - void evaluateRegionSafety(); - std::vector getSortedCastleList( const VecCastles & castles, const std::set & castlesInDanger ); - void resetPathfinder() override; - bool isValidHeroObject( const Heroes & hero, const int32_t index, const bool underHero ) override; void battleBegins() override; void tradingPostVisitEvent( Kingdom & kingdom ) override; + bool isValidHeroObject( const Heroes & hero, const int32_t index, const bool underHero ) override; + double getObjectValue( const Heroes & hero, const int index, const int objectType, const double valueToIgnore, const uint32_t distanceToObject ) const; double getTargetArmyStrength( const Maps::Tiles & tile, const MP2::MapObjectType objectType ); - bool isPriorityTask( const int32_t index ) const - { - return _priorityTargets.find( index ) != _priorityTargets.end(); - } - - bool isCriticalTask( const int32_t index ) const - { - const auto iter = _priorityTargets.find( index ); - if ( iter == _priorityTargets.end() ) { - return false; - } - - return iter->second.type == PriorityTaskType::ATTACK || iter->second.type == PriorityTaskType::DEFEND; - } - private: void CastleTurn( Castle & castle, const bool defensiveStrategy ); // Returns true if heroes can still do tasks but they have no move points. bool HeroesTurn( VecHeroes & heroes, const uint32_t startProgressValue, const uint32_t endProgressValue ); + bool recruitHero( Castle & castle, bool buyArmy ); + void reinforceHeroInCastle( Heroes & hero, Castle & castle, const Funds & budget ); + + void evaluateRegionSafety(); + + std::vector getSortedCastleList( const VecCastles & castles, const std::set & castlesInDanger ); + int getPriorityTarget( const HeroToMove & heroInfo, double & maxPriority ); double getGeneralObjectValue( const Heroes & hero, const int index, const double valueToIgnore, const uint32_t distanceToObject ) const; @@ -332,7 +320,6 @@ namespace AI std::set findCastlesInDanger( const Kingdom & kingdom ); void updatePriorityForEnemyArmy( const Kingdom & kingdom, const EnemyArmy & enemyArmy ); - void updatePriorityForCastle( const Castle & castle ); // Return true if the castle is in danger. @@ -340,9 +327,23 @@ namespace AI bool updateIndividualPriorityForCastle( const Castle & castle, const EnemyArmy & enemyArmy ); void removePriorityAttackTarget( const int32_t tileIndex ); - void updatePriorityAttackTarget( const Kingdom & kingdom, const Maps::Tiles & tile ); + bool isPriorityTask( const int32_t index ) const + { + return _priorityTargets.find( index ) != _priorityTargets.end(); + } + + bool isCriticalTask( const int32_t index ) const + { + const auto iter = _priorityTargets.find( index ); + if ( iter == _priorityTargets.end() ) { + return false; + } + + return iter->second.type == PriorityTaskType::ATTACK || iter->second.type == PriorityTaskType::DEFEND; + } + // The following member variables should not be saved or serialized std::vector _mapActionObjects; std::map _priorityTargets; diff --git a/src/fheroes2/ai/normal/ai_normal_castle.cpp b/src/fheroes2/ai/normal/ai_normal_castle.cpp index b2c3894da3d..076adf302d8 100644 --- a/src/fheroes2/ai/normal/ai_normal_castle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_castle.cpp @@ -314,31 +314,26 @@ namespace AI void Normal::CastleTurn( Castle & castle, const bool defensiveStrategy ) { if ( defensiveStrategy ) { - // Avoid building monster dwellings when defensive as they might fall into enemy's hands, unless we have a lot of resources. - const Kingdom & kingdom = castle.GetKingdom(); + const Funds & kingdomFunds = castle.GetKingdom().GetFunds(); - // TODO: check if we can upgrade monsters. It is much cheaper (except Giants into Titans) to upgrade monsters than buy new ones. + // If the castle is potentially under threat, then it makes sense to try to hire the maximum number of troops so that the enemy cannot hire them even if he + // captures the castle, therefore, it is worth starting with hiring. + castle.recruitBestAvailable( kingdomFunds ); - Troops possibleReinforcement = castle.getAvailableArmy( kingdom.GetFunds() ); - double possibleReinforcementStrength = possibleReinforcement.GetStrength(); + Army & garrison = castle.GetArmy(); - // A very rough estimation of strength. We measure the strength of possible army to hire with the strength of purchasing a turret. - const Battle::Tower tower( castle, Battle::TowerType::TWR_RIGHT, 0 ); - const Troop towerMonster( Monster::ARCHER, tower.GetCount() ); - const double towerStrength = towerMonster.GetStrength(); - if ( possibleReinforcementStrength > towerStrength ) { - castle.recruitBestAvailable( kingdom.GetFunds() ); - OptimizeTroopsOrder( castle.GetArmy() ); - } + // Then we try to upgrade the existing units in the castle garrison... + garrison.UpgradeTroops( castle ); - if ( castle.GetActualArmy().getTotalCount() > 0 ) { - Build( castle, defensiveStructures ); - } + // ... and then we try to hire troops again, because after upgrading the existing troops, there could be a place for new units. + castle.recruitBestAvailable( kingdomFunds ); - castle.recruitBestAvailable( kingdom.GetFunds() ); - OptimizeTroopsOrder( castle.GetArmy() ); + OptimizeTroopsOrder( garrison ); + // Avoid building monster dwellings when defensive as they might fall into enemy's hands, unless we have a lot of resources. Instead, try to build defensive + // structures if there is at least some kind of garrison in the castle. if ( castle.GetActualArmy().getTotalCount() > 0 ) { + Build( castle, defensiveStructures ); Build( castle, supportingDefensiveStructures ); } } diff --git a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp index 62e11b7547e..db5dda86314 100644 --- a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp +++ b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp @@ -269,7 +269,7 @@ namespace namespace AI { - bool Normal::recruitHero( Castle & castle, bool buyArmy, bool underThreat ) + bool Normal::recruitHero( Castle & castle, bool buyArmy ) { Kingdom & kingdom = castle.GetKingdom(); const Recruits & rec = kingdom.GetRecruits(); @@ -310,7 +310,6 @@ namespace AI } if ( buyArmy ) { - CastleTurn( castle, underThreat ); reinforceHeroInCastle( *recruit, castle, kingdom.GetFunds() ); } else { @@ -928,13 +927,15 @@ namespace AI bool Normal::purchaseNewHeroes( const std::vector & sortedCastleList, const std::set & castlesInDanger, const int32_t availableHeroCount, const bool moreTasksForHeroes ) { - const bool slowEarlyGame = world.CountDay() < 5 && sortedCastleList.size() == 1; + const bool isEarlyGameWithSingleCastle = world.CountDay() < 5 && sortedCastleList.size() == 1; int32_t heroLimit = world.w() / Maps::SMALL + 1; - if ( _personality == EXPLORER ) + if ( _personality == EXPLORER ) { ++heroLimit; - if ( slowEarlyGame ) + } + if ( isEarlyGameWithSingleCastle ) { heroLimit = 2; + } if ( availableHeroCount >= heroLimit ) { return false; @@ -977,7 +978,7 @@ namespace AI } // target found, buy hero - return recruitmentCastle && recruitHero( *recruitmentCastle, !slowEarlyGame, false ); + return recruitmentCastle && recruitHero( *recruitmentCastle, !isEarlyGameWithSingleCastle ); } void Normal::tradingPostVisitEvent( Kingdom & /*kingdom*/ ) diff --git a/src/fheroes2/kingdom/kingdom.h b/src/fheroes2/kingdom/kingdom.h index 6f48359cf9b..7a710804a6c 100644 --- a/src/fheroes2/kingdom/kingdom.h +++ b/src/fheroes2/kingdom/kingdom.h @@ -115,6 +115,7 @@ class Kingdom : public BitModes, public Control { return resource; } + Funds GetIncome( int type = INCOME_ALL ) const; double GetArmiesStrength() const; From ff6fcfadfa7f50f5867c8de0a245bccc4d1c9c91 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 2 Mar 2024 01:37:44 +0300 Subject: [PATCH 14/40] Apply IWYU suggestion --- src/fheroes2/ai/normal/ai_normal_castle.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_castle.cpp b/src/fheroes2/ai/normal/ai_normal_castle.cpp index 076adf302d8..a4ec4866134 100644 --- a/src/fheroes2/ai/normal/ai_normal_castle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_castle.cpp @@ -28,8 +28,6 @@ #include "ai.h" #include "ai_normal.h" #include "army.h" -#include "army_troop.h" -#include "battle_tower.h" #include "castle.h" #include "difficulty.h" #include "game.h" From d0ac6a863de23bab2342f1b4f7555b93ba2ae97f Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 2 Mar 2024 02:28:47 +0300 Subject: [PATCH 15/40] Tune the AI income bonus --- src/fheroes2/game/difficulty.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index d0cda474f43..7f8d4a8a13a 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -77,10 +77,12 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty ) }; switch ( difficulty ) { - case Difficulty::EXPERT: + case Difficulty::HARD: return getIncomeFromSetsOfResourceMines( 1 ); - case Difficulty::IMPOSSIBLE: + case Difficulty::EXPERT: return getIncomeFromSetsOfResourceMines( 2 ); + case Difficulty::IMPOSSIBLE: + return getIncomeFromSetsOfResourceMines( 3 ); default: break; } From af69614a2e822f610b6fedcde829365ec1199f32 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 2 Mar 2024 11:46:24 +0300 Subject: [PATCH 16/40] Change the surrendering logic --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 33 ++++++++--------- src/fheroes2/army/army.cpp | 41 +++++++-------------- src/fheroes2/army/army.h | 5 +-- src/fheroes2/battle/battle_army.cpp | 19 ++++++++-- src/fheroes2/battle/battle_army.h | 4 +- src/fheroes2/game/difficulty.cpp | 10 ----- src/fheroes2/game/difficulty.h | 3 -- 7 files changed, 47 insertions(+), 68 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 4962cd91e98..227e9990551 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -600,7 +600,6 @@ namespace AI } 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 ) { @@ -618,13 +617,10 @@ namespace AI } ); }(); + const Force & force = arena.getForce( _myColor ); const Kingdom & kingdom = actualHero->GetKingdom(); - const bool considerRetreat = [this, &arena, actualHero, gameDifficulty, isGameCampaign, hasValuableArtifacts, &kingdom]() { - if ( !Difficulty::allowAIToRetreat( gameDifficulty, isGameCampaign ) ) { - return false; - } - + const bool considerRetreat = [this, &arena, actualHero, gameDifficulty, hasValuableArtifacts, &kingdom]() { if ( !arena.CanRetreatOpponent( _myColor ) ) { return false; } @@ -648,11 +644,7 @@ namespace AI return actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty ); }(); - const bool considerSurrender = [this, &arena, actualHero, gameDifficulty, isGameCampaign, hasValuableArtifacts, &kingdom]() { - if ( !Difficulty::allowAIToSurrender( gameDifficulty, isGameCampaign ) ) { - return false; - } - + const bool considerSurrender = [this, &arena, actualHero, gameDifficulty, hasValuableArtifacts, &force, &kingdom]() { if ( !arena.CanSurrenderOpponent( _myColor ) ) { return false; } @@ -682,10 +674,19 @@ namespace AI } // 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 ); - }(); + if ( actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty ) ) { + return true; + } - const Force & force = arena.getForce( _myColor ); + // Otherwise, if some high-level units remain in the hero's army after the surrender, then it makes sense to keep them + const std::vector remainingTroops = force.getTroopsRemainingInCaseOfSurrender(); + if ( std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ) ) { + return true; + } + + // Otherwise, there is no point in surrendering + return false; + }(); if ( !considerRetreat ) { if ( !considerSurrender ) { @@ -703,10 +704,6 @@ namespace AI return Outcome::Retreat; } - if ( force.getStrengthOfArmyRemainingInCaseOfSurrender() < Army::getStrengthOfAverageStartingArmy( actualHero ) ) { - return Outcome::Retreat; - } - if ( !kingdom.AllowPayment( Funds{ Resource::GOLD, force.GetSurrenderCost() } * Difficulty::getGoldReserveRatioForAISurrender( gameDifficulty ) ) ) { return Outcome::Retreat; } diff --git a/src/fheroes2/army/army.cpp b/src/fheroes2/army/army.cpp index d0084238064..b8d002c73e0 100644 --- a/src/fheroes2/army/army.cpp +++ b/src/fheroes2/army/army.cpp @@ -239,17 +239,19 @@ Troops::~Troops() } ); } -void Troops::Assign( const Troop * it1, const Troop * it2 ) +void Troops::Assign( const Troop * itbeg, const Troop * itend ) { Clean(); iterator it = begin(); - while ( it != end() && it1 != it2 ) { - if ( it1->isValid() ) - ( *it )->Set( *it1 ); + while ( it != end() && itbeg != itend ) { + if ( itbeg->isValid() ) { + ( *it )->Set( *itbeg ); + } + ++it; - ++it1; + ++itbeg; } } @@ -281,10 +283,13 @@ void Troops::PushBack( const Monster & mons, uint32_t count ) void Troops::PopBack() { - if ( !empty() ) { - delete back(); - pop_back(); + if ( empty() ) { + return; } + + delete back(); + + pop_back(); } Troop * Troops::GetTroop( size_t pos ) @@ -1306,26 +1311,6 @@ double Army::GetStrength() const return result; } -double Army::getStrengthOfAverageStartingArmy( const Heroes * hero ) -{ - assert( hero != nullptr ); - - const int race = hero->GetRace(); - - double result = 0.0; - - for ( uint32_t dwelling : std::array{ DWELLING_MONSTER1, DWELLING_MONSTER2 } ) { - const Monster monster{ race, dwelling }; - assert( monster.isValid() ); - - const auto [min, max] = getNumberOfMonstersInStartingArmy( monster ); - - result += Troop{ monster, ( min + max ) / 2 }.GetStrength(); - } - - return result; -} - void Army::Reset( const bool defaultArmy /* = false */ ) { Troops::Clean(); diff --git a/src/fheroes2/army/army.h b/src/fheroes2/army/army.h index 041d77d800b..49e87431c1c 100644 --- a/src/fheroes2/army/army.h +++ b/src/fheroes2/army/army.h @@ -56,7 +56,7 @@ class Troops : protected std::vector Troops & operator=( const Troops & ) = delete; - void Assign( const Troop *, const Troop * ); + void Assign( const Troop * itbeg, const Troop * itend ); void Assign( const Troops & ); void Insert( const Troops & ); void PushBack( const Monster &, uint32_t ); @@ -173,9 +173,6 @@ class Army final : public Troops, public Control static NeutralMonsterJoiningCondition GetJoinSolution( const Heroes &, const Maps::Tiles &, const Troop & ); - // Returns the strength of the average starting army for a given hero (not taking into account the hero's bonuses) - static double getStrengthOfAverageStartingArmy( const Heroes * hero ); - static void drawSingleDetailedMonsterLine( const Troops & troops, int32_t cx, int32_t cy, int32_t width ); static void drawMultipleMonsterLines( const Troops & troops, int32_t posX, int32_t posY, int32_t lineWidth, bool isCompact, const bool isDetailedView, const bool isGarrisonView = false, const uint32_t thievesGuildsCount = 0 ); diff --git a/src/fheroes2/battle/battle_army.cpp b/src/fheroes2/battle/battle_army.cpp index 80026aa50e4..9fbe699d855 100644 --- a/src/fheroes2/battle/battle_army.cpp +++ b/src/fheroes2/battle/battle_army.cpp @@ -307,9 +307,10 @@ void Battle::Force::SyncArmyCount() } } -double Battle::Force::getStrengthOfArmyRemainingInCaseOfSurrender() const +std::vector Battle::Force::getTroopsRemainingInCaseOfSurrender() const { - double result = 0.0; + std::vector result; + result.reserve( army.Size() ); // Consider only the state of the original army for ( uint32_t index = 0; index < army.Size(); ++index ) { @@ -323,10 +324,22 @@ double Battle::Force::getStrengthOfArmyRemainingInCaseOfSurrender() const continue; } + const Monster mons = unit->GetMonster(); + if ( !mons.isValid() ) { + continue; + } + // Consider only the number of units that will remain in the army after the end of the battle (in particular, don't take into account the number of // non-true-resurrected units) - result += Troop{ unit->GetMonster(), unit->GetDead() > unit->GetInitialCount() ? 0 : unit->GetInitialCount() - unit->GetDead() }.GetStrength(); + const uint32_t count = ( unit->GetDead() > unit->GetInitialCount() ? 0 : unit->GetInitialCount() - unit->GetDead() ); + if ( count == 0 ) { + continue; + } + + result.emplace_back( mons, count ); } + assert( std::all_of( result.begin(), result.end(), []( const Troop & troop ) { return troop.isValid(); } ) ); + return result; } diff --git a/src/fheroes2/battle/battle_army.h b/src/fheroes2/battle/battle_army.h index d57f8b4f43e..84108e1af36 100644 --- a/src/fheroes2/battle/battle_army.h +++ b/src/fheroes2/battle/battle_army.h @@ -142,8 +142,8 @@ namespace Battle // Returns the cost of surrender (in units of gold) for the current army on the battlefield uint32_t GetSurrenderCost() const; - // Returns the strength of the army that will remain in case of surrender (not taking into account the hero's bonuses) - double getStrengthOfArmyRemainingInCaseOfSurrender() const; + // Returns the troops that will remain in case of surrender. Only valid troops are returned (no empty slots allowed). + std::vector getTroopsRemainingInCaseOfSurrender() const; Troops GetKilledTroops() const; diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 7f8d4a8a13a..e01f66be3a6 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -127,16 +127,6 @@ int Difficulty::GetHeroMovementBonusForAI( 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; diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index 3af4a7ef3f2..6eab97f94b5 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -57,9 +57,6 @@ namespace Difficulty int GetHeroMovementBonusForAI( int difficulty ); - bool allowAIToRetreat( const int difficulty, const bool isCampaign ); - bool allowAIToSurrender( const int difficulty, const bool isCampaign ); - // Returns the minimum hero level at which the AI can consider the possibility of surrender or retreat from the battlefield for this hero int getMinHeroLevelForAIRetreat( const int difficulty ); From 657dd7ef10bf0bc93e3fa46fe5f36bac0684575d Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 2 Mar 2024 12:33:12 +0300 Subject: [PATCH 17/40] Simplify the logic --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 227e9990551..3565138162b 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -680,12 +680,7 @@ namespace AI // Otherwise, if some high-level units remain in the hero's army after the surrender, then it makes sense to keep them const std::vector remainingTroops = force.getTroopsRemainingInCaseOfSurrender(); - if ( std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ) ) { - return true; - } - - // Otherwise, there is no point in surrendering - return false; + return std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ); }(); if ( !considerRetreat ) { From 49a5a707feab5a2337136bebac6b88b1806ed7e5 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 2 Mar 2024 12:35:39 +0300 Subject: [PATCH 18/40] Apply IWYU suggestions --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 2 +- src/fheroes2/army/army.cpp | 1 - src/fheroes2/battle/battle_army.h | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 3565138162b..fc0a3d2476e 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -38,7 +38,7 @@ #include "ai.h" #include "ai_normal.h" -#include "army.h" +#include "army_troop.h" #include "artifact.h" #include "artifact_info.h" #include "battle.h" diff --git a/src/fheroes2/army/army.cpp b/src/fheroes2/army/army.cpp index b8d002c73e0..b65046adb6f 100644 --- a/src/fheroes2/army/army.cpp +++ b/src/fheroes2/army/army.cpp @@ -24,7 +24,6 @@ #include "army.h" #include -#include #include #include #include diff --git a/src/fheroes2/battle/battle_army.h b/src/fheroes2/battle/battle_army.h index 84108e1af36..684fb0f19b6 100644 --- a/src/fheroes2/battle/battle_army.h +++ b/src/fheroes2/battle/battle_army.h @@ -38,6 +38,7 @@ #include "monster.h" class HeroBase; +class Troop; namespace Battle { From 598da2cbca9923582713b9083cb8a5d36cb112bb Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 2 Mar 2024 14:42:40 +0300 Subject: [PATCH 19/40] Tune the surrender logic --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 112 +++++++++++--------- src/fheroes2/game/difficulty.cpp | 5 - src/fheroes2/game/difficulty.h | 3 - 3 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index fc0a3d2476e..12ee92d6edf 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -620,90 +620,100 @@ namespace AI const Force & force = arena.getForce( _myColor ); const Kingdom & kingdom = actualHero->GetKingdom(); - const bool considerRetreat = [this, &arena, actualHero, gameDifficulty, hasValuableArtifacts, &kingdom]() { - if ( !arena.CanRetreatOpponent( _myColor ) ) { + const bool isAbleToSurrender = [this, &arena, &force, &kingdom]() { + if ( !arena.CanSurrenderOpponent( _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 ( kingdom.AllowPayment( { Resource::GOLD, force.GetSurrenderCost() } ) ); + }(); + + const bool isPossibleToReHire = [actualHero, &kingdom]() { + // If the hero is not the last in his kingdom, then we will assume that there is a possibility of re-hiring - even if there are no castles in the + // kingdom, one of the remaining heroes can capture an enemy castle + const VecHeroes & heroes = kingdom.GetHeroes(); + if ( heroes.size() > 1 ) { 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 ); + assert( heroes.size() == 1 && heroes.at( 0 ) == actualHero ); - if ( kingdom.GetCastles().empty() ) { - return false; - } + // Otherwise, if this hero is the last one, and there are no castles in the kingdom, then it will be impossible to re-hire this hero + const VecCastles & castles = kingdom.GetCastles(); + if ( castles.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 ); - }(); + // Otherwise, if this hero is defending the last castle, then it will be impossible to re-hire this hero + const Castle * castle = actualHero->inCastle(); + if ( castle && castles.size() == 1 ) { + assert( castles.at( 0 ) == castle ); - const bool considerSurrender = [this, &arena, actualHero, gameDifficulty, hasValuableArtifacts, &force, &kingdom]() { - 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 ); + // Otherwise, assume that there is a possibility of re-hiring + return true; + }(); - const VecCastles & castles = kingdom.GetCastles(); - if ( castles.empty() ) { - return false; - } + if ( !arena.CanRetreatOpponent( _myColor ) ) { + if ( !isAbleToSurrender ) { + return Outcome::ContinueBattle; + } - const Castle * castle = actualHero->inCastle(); - if ( castle && castles.size() == 1 ) { - assert( castles.at( 0 ) == castle ); + // If the hero has valuable artifacts, he should surrender 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 Outcome::Surrender; + } - return false; - } + // Otherwise, if this hero cannot be rehired, then there is no point in surrendering + if ( !isPossibleToReHire ) { + return Outcome::ContinueBattle; } - // Otherwise, if this hero is relatively experienced, then he should think about surrendering so that he can be hired again later + // Otherwise, if this hero is relatively experienced, then he should surrender so that he can be hired again later if ( actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty ) ) { - return true; + return Outcome::Surrender; } - // Otherwise, if some high-level units remain in the hero's army after the surrender, then it makes sense to keep them + // Otherwise, if some high-level units will remain in the hero's army after the surrender, then it makes sense to save them by surrendering const std::vector remainingTroops = force.getTroopsRemainingInCaseOfSurrender(); - return std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ); - }(); - - if ( !considerRetreat ) { - if ( !considerSurrender ) { - return Outcome::ContinueBattle; + if ( std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ) ) { + return Outcome::Surrender; } - if ( !kingdom.AllowPayment( { Resource::GOLD, force.GetSurrenderCost() } ) ) { - return Outcome::ContinueBattle; - } + // Otherwise, there is no point in surrendering + return Outcome::ContinueBattle; + } - return Outcome::Surrender; + // If the hero is able to surrender, can be rehired, and some high-level units will remain in his army after the surrender, then it makes sense to save + // them by surrendering + if ( isAbleToSurrender && isPossibleToReHire ) { + const std::vector remainingTroops = force.getTroopsRemainingInCaseOfSurrender(); + if ( std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ) ) { + return Outcome::Surrender; + } } - if ( !considerSurrender ) { + // Otherwise, if this hero has valuable artifacts, he should retreat 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 Outcome::Retreat; } - if ( !kingdom.AllowPayment( Funds{ Resource::GOLD, force.GetSurrenderCost() } * Difficulty::getGoldReserveRatioForAISurrender( gameDifficulty ) ) ) { + // Otherwise, if this hero cannot be rehired, then there is no point in retreating + if ( !isPossibleToReHire ) { + return Outcome::ContinueBattle; + } + + // Otherwise, if this hero is relatively experienced, then he should retreat so that he can be hired again later + if ( actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty ) ) { return Outcome::Retreat; } - return Outcome::Surrender; + // Otherwise, there is no point in retreating or surrendering + return Outcome::ContinueBattle; }(); const auto farewellSpellcast = [this, &arena, ¤tUnit, &actions]() { diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index e01f66be3a6..2e2d75fa71a 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -149,11 +149,6 @@ double Difficulty::getArmyStrengthRatioForAIRetreat( const int difficulty ) return 100.0 / 6.0; } -uint32_t Difficulty::getGoldReserveRatioForAISurrender( const int /* difficulty */ ) -{ - return 10; -} - uint32_t Difficulty::GetDimensionDoorLimitForAI( int difficulty ) { switch ( difficulty ) { diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index 6eab97f94b5..d0db0063d5d 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -63,9 +63,6 @@ namespace Difficulty // Returns the ratio of the strength of the enemy army to the strength of the AI army, above which the AI decides to surrender or retreat from the battlefield double getArmyStrengthRatioForAIRetreat( const int difficulty ); - // Returns the minimum ratio of the AI kingdom's gold reserve to the cost of surrender, at which the AI will prefer surrender to retreat from the battlefield - uint32_t getGoldReserveRatioForAISurrender( const int difficulty ); - // Returns the limit on the number of times the Dimension Door spell can be cast, which is applied to each of the AI-controlled heroes individually during one AI // turn uint32_t GetDimensionDoorLimitForAI( int difficulty ); From 55ea609fc7fcae4d439e7d7faeeacc15a2e6565f Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 2 Mar 2024 15:34:46 +0300 Subject: [PATCH 20/40] Reduce the code duplication --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 12ee92d6edf..19236e44897 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -656,6 +656,12 @@ namespace AI return true; }(); + const auto checkThatRemainingTroopsAreWorthSaving = [&force]() { + const std::vector remainingTroops = force.getTroopsRemainingInCaseOfSurrender(); + + return std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ); + }; + if ( !arena.CanRetreatOpponent( _myColor ) ) { if ( !isAbleToSurrender ) { return Outcome::ContinueBattle; @@ -677,9 +683,8 @@ namespace AI return Outcome::Surrender; } - // Otherwise, if some high-level units will remain in the hero's army after the surrender, then it makes sense to save them by surrendering - const std::vector remainingTroops = force.getTroopsRemainingInCaseOfSurrender(); - if ( std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ) ) { + // Otherwise, if this hero's remaining troops are worth saving, then it makes sense to save them by surrendering + if ( checkThatRemainingTroopsAreWorthSaving() ) { return Outcome::Surrender; } @@ -687,13 +692,9 @@ namespace AI return Outcome::ContinueBattle; } - // If the hero is able to surrender, can be rehired, and some high-level units will remain in his army after the surrender, then it makes sense to save - // them by surrendering - if ( isAbleToSurrender && isPossibleToReHire ) { - const std::vector remainingTroops = force.getTroopsRemainingInCaseOfSurrender(); - if ( std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ) ) { - return Outcome::Surrender; - } + // If the hero is able to surrender, can be rehired, and his remaining troops are worth saving, then it makes sense to save them by surrendering + if ( isAbleToSurrender && isPossibleToReHire && checkThatRemainingTroopsAreWorthSaving() ) { + return Outcome::Surrender; } // Otherwise, if this hero has valuable artifacts, he should retreat so that these artifacts do not end up at the disposal of the enemy, especially in the From 7b4863dd91da7912f7c32eb4dee87f6ec85b2d47 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Wed, 6 Mar 2024 12:44:09 +0300 Subject: [PATCH 21/40] Do not allow AI to surrender in the open field --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 21 ++---------- src/fheroes2/battle/battle_army.cpp | 37 --------------------- src/fheroes2/battle/battle_army.h | 4 --- 3 files changed, 2 insertions(+), 60 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 19236e44897..9c4d80d4ad5 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -38,7 +38,6 @@ #include "ai.h" #include "ai_normal.h" -#include "army_troop.h" #include "artifact.h" #include "artifact_info.h" #include "battle.h" @@ -656,12 +655,6 @@ namespace AI return true; }(); - const auto checkThatRemainingTroopsAreWorthSaving = [&force]() { - const std::vector remainingTroops = force.getTroopsRemainingInCaseOfSurrender(); - - return std::any_of( remainingTroops.begin(), remainingTroops.end(), []( const Troop & troop ) { return troop.GetMonsterLevel() >= 5; } ); - }; - if ( !arena.CanRetreatOpponent( _myColor ) ) { if ( !isAbleToSurrender ) { return Outcome::ContinueBattle; @@ -683,22 +676,12 @@ namespace AI return Outcome::Surrender; } - // Otherwise, if this hero's remaining troops are worth saving, then it makes sense to save them by surrendering - if ( checkThatRemainingTroopsAreWorthSaving() ) { - return Outcome::Surrender; - } - // Otherwise, there is no point in surrendering return Outcome::ContinueBattle; } - // If the hero is able to surrender, can be rehired, and his remaining troops are worth saving, then it makes sense to save them by surrendering - if ( isAbleToSurrender && isPossibleToReHire && checkThatRemainingTroopsAreWorthSaving() ) { - return Outcome::Surrender; - } - - // Otherwise, if this hero has valuable artifacts, he should retreat so that these artifacts do not end up at the disposal of the enemy, especially in the - // case of an alliance war + // If the hero has valuable artifacts, he should retreat 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 Outcome::Retreat; } diff --git a/src/fheroes2/battle/battle_army.cpp b/src/fheroes2/battle/battle_army.cpp index 9fbe699d855..7bc3a45de9c 100644 --- a/src/fheroes2/battle/battle_army.cpp +++ b/src/fheroes2/battle/battle_army.cpp @@ -306,40 +306,3 @@ void Battle::Force::SyncArmyCount() troop->SetCount( unit->GetDead() > unit->GetInitialCount() ? 0 : unit->GetInitialCount() - unit->GetDead() ); } } - -std::vector Battle::Force::getTroopsRemainingInCaseOfSurrender() const -{ - std::vector result; - result.reserve( army.Size() ); - - // Consider only the state of the original army - for ( uint32_t index = 0; index < army.Size(); ++index ) { - const Troop * troop = army.GetTroop( index ); - if ( troop == nullptr || !troop->isValid() ) { - continue; - } - - const Unit * unit = FindUID( uids.at( index ) ); - if ( unit == nullptr ) { - continue; - } - - const Monster mons = unit->GetMonster(); - if ( !mons.isValid() ) { - continue; - } - - // Consider only the number of units that will remain in the army after the end of the battle (in particular, don't take into account the number of - // non-true-resurrected units) - const uint32_t count = ( unit->GetDead() > unit->GetInitialCount() ? 0 : unit->GetInitialCount() - unit->GetDead() ); - if ( count == 0 ) { - continue; - } - - result.emplace_back( mons, count ); - } - - assert( std::all_of( result.begin(), result.end(), []( const Troop & troop ) { return troop.isValid(); } ) ); - - return result; -} diff --git a/src/fheroes2/battle/battle_army.h b/src/fheroes2/battle/battle_army.h index 684fb0f19b6..b44b72b58a6 100644 --- a/src/fheroes2/battle/battle_army.h +++ b/src/fheroes2/battle/battle_army.h @@ -38,7 +38,6 @@ #include "monster.h" class HeroBase; -class Troop; namespace Battle { @@ -143,9 +142,6 @@ namespace Battle // Returns the cost of surrender (in units of gold) for the current army on the battlefield uint32_t GetSurrenderCost() const; - // Returns the troops that will remain in case of surrender. Only valid troops are returned (no empty slots allowed). - std::vector getTroopsRemainingInCaseOfSurrender() const; - Troops GetKilledTroops() const; bool animateIdleUnits() const; From 1c3f408f8225402c013ac46202ba9fc63fc58d6e Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Wed, 13 Mar 2024 14:32:29 +0300 Subject: [PATCH 22/40] Use sum of primary skills instead of hero's level --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 6 ++++-- src/fheroes2/game/difficulty.cpp | 5 ----- src/fheroes2/game/difficulty.h | 3 --- src/fheroes2/heroes/heroes.cpp | 2 +- src/fheroes2/heroes/skill.h | 6 ++++++ 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 9c4d80d4ad5..b8c6d188899 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -655,6 +655,8 @@ namespace AI return true; }(); + const int minHeroTotalPrimarySkillLevelForRetreat = 10; + if ( !arena.CanRetreatOpponent( _myColor ) ) { if ( !isAbleToSurrender ) { return Outcome::ContinueBattle; @@ -672,7 +674,7 @@ namespace AI } // Otherwise, if this hero is relatively experienced, then he should surrender so that he can be hired again later - if ( actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty ) ) { + if ( actualHero->getTotalPrimarySkillLevel() >= minHeroTotalPrimarySkillLevelForRetreat ) { return Outcome::Surrender; } @@ -692,7 +694,7 @@ namespace AI } // Otherwise, if this hero is relatively experienced, then he should retreat so that he can be hired again later - if ( actualHero->GetLevel() >= Difficulty::getMinHeroLevelForAIRetreat( gameDifficulty ) ) { + if ( actualHero->getTotalPrimarySkillLevel() >= minHeroTotalPrimarySkillLevelForRetreat ) { return Outcome::Retreat; } diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 2e2d75fa71a..f482ed746b1 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -127,11 +127,6 @@ int Difficulty::GetHeroMovementBonusForAI( int difficulty ) return 0; } -int Difficulty::getMinHeroLevelForAIRetreat( const int /* difficulty */ ) -{ - return 3; -} - double Difficulty::getArmyStrengthRatioForAIRetreat( const int difficulty ) { switch ( difficulty ) { diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index d0db0063d5d..ebdb7e6dd05 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -57,9 +57,6 @@ namespace Difficulty int GetHeroMovementBonusForAI( int difficulty ); - // Returns the minimum hero level at which the AI can consider the possibility of surrender or retreat from the battlefield for this hero - int getMinHeroLevelForAIRetreat( const int difficulty ); - // Returns the ratio of the strength of the enemy army to the strength of the AI army, above which the AI decides to surrender or retreat from the battlefield double getArmyStrengthRatioForAIRetreat( const int difficulty ); diff --git a/src/fheroes2/heroes/heroes.cpp b/src/fheroes2/heroes/heroes.cpp index 863d52c390e..2a5913fb723 100644 --- a/src/fheroes2/heroes/heroes.cpp +++ b/src/fheroes2/heroes/heroes.cpp @@ -653,7 +653,7 @@ int Heroes::GetManaIndexSprite() const int Heroes::getStatsValue() const { // experience and artifacts don't matter here, only natural stats - return attack + defense + power + knowledge + secondary_skills.GetTotalLevel(); + return getTotalPrimarySkillLevel() + secondary_skills.GetTotalLevel(); } double Heroes::getRecruitValue() const diff --git a/src/fheroes2/heroes/skill.h b/src/fheroes2/heroes/skill.h index a6a7102fb89..e3dccc6997b 100644 --- a/src/fheroes2/heroes/skill.h +++ b/src/fheroes2/heroes/skill.h @@ -186,6 +186,12 @@ namespace Skill int LevelUp( int race, int level, uint32_t seed ); + // Returns the sum of the values of the four primary skills (attack, defense, power and knowledge), belonging directly to the hero (i.e. excluding artifacts) + int getTotalPrimarySkillLevel() const + { + return attack + defense + power + knowledge; + }; + static const char * String( const int skillType ); static std::string StringDescription( int, const Heroes * ); static int GetInitialSpell( int race ); From 2fb2a60e81de8216e18de3556ace39f53320924f Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Wed, 13 Mar 2024 15:00:19 +0300 Subject: [PATCH 23/40] Remove extra semicolon --- src/fheroes2/heroes/skill.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/heroes/skill.h b/src/fheroes2/heroes/skill.h index e3dccc6997b..560ac2cc145 100644 --- a/src/fheroes2/heroes/skill.h +++ b/src/fheroes2/heroes/skill.h @@ -190,7 +190,7 @@ namespace Skill int getTotalPrimarySkillLevel() const { return attack + defense + power + knowledge; - }; + } static const char * String( const int skillType ); static std::string StringDescription( int, const Heroes * ); From 87ccd2cad29281a0fa0c7c6c6194b38adf465d00 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Wed, 13 Mar 2024 15:26:46 +0300 Subject: [PATCH 24/40] Remove a lot of (now) unused code --- src/fheroes2/castle/castle.cpp | 56 ---------------------------- src/fheroes2/castle/castle.h | 8 ---- src/fheroes2/game/difficulty.cpp | 5 --- src/fheroes2/game/difficulty.h | 3 -- src/fheroes2/game/game_startgame.cpp | 10 ----- src/fheroes2/world/world.cpp | 7 ---- src/fheroes2/world/world.h | 1 - 7 files changed, 90 deletions(-) diff --git a/src/fheroes2/castle/castle.cpp b/src/fheroes2/castle/castle.cpp index 5bf9a32aa29..07357697ac4 100644 --- a/src/fheroes2/castle/castle.cpp +++ b/src/fheroes2/castle/castle.cpp @@ -901,62 +901,6 @@ void Castle::ActionNewWeek() } } -void Castle::ActionNewWeekAIBonuses() -{ - if ( world.GetWeekType().GetType() == WeekName::PLAGUE ) { - // No growth bonus can be applied. - return; - } - - if ( !isControlAI() ) { - // No AI - no perks! - return; - } - - if ( GetColor() == Color::NONE ) { - // Neutrals aren't considered as AI players. - return; - } - - static const std::array basicDwellings - = { DWELLING_MONSTER1, DWELLING_MONSTER2, DWELLING_MONSTER3, DWELLING_MONSTER4, DWELLING_MONSTER5, DWELLING_MONSTER6 }; - - for ( const building_t dwellingId : basicDwellings ) { - uint32_t * dwellingMonsters = GetDwelling( dwellingId ); - if ( dwellingMonsters == nullptr ) { - // Such dwelling (or its upgrade) has not been built. - continue; - } - - uint32_t originalGrowth = Monster( race, GetActualDwelling( dwellingId ) ).GetGrown(); - - if ( building & BUILD_WELL ) { - originalGrowth += GetGrownWell(); - } - - if ( ( dwellingId == DWELLING_MONSTER1 ) && ( building & BUILD_WEL2 ) ) { - originalGrowth += GetGrownWel2(); - } - - if ( originalGrowth == 0 ) { - continue; - } - - const long bonusGrowth = std::lround( originalGrowth * Difficulty::GetUnitGrowthBonusForAI( Game::getDifficulty(), Game::isCampaign(), race, dwellingId ) ); - if ( bonusGrowth >= 0 ) { - *dwellingMonsters += bonusGrowth; - - continue; - } - - // If the original unit growth is non-zero, then the total unit growth after the application of penalties should be at least one unit - const uint32_t growthPenalty = std::min( static_cast( -bonusGrowth ), originalGrowth - 1 ); - assert( *dwellingMonsters > growthPenalty ); - - *dwellingMonsters -= growthPenalty; - } -} - void Castle::ActionNewMonth() const { // Do nothing. diff --git a/src/fheroes2/castle/castle.h b/src/fheroes2/castle/castle.h index 695541d71a5..0b08d4a1118 100644 --- a/src/fheroes2/castle/castle.h +++ b/src/fheroes2/castle/castle.h @@ -231,10 +231,7 @@ class Castle : public MapPosition, public BitModes, public ColorBase, public Con void ChangeColor( int ); void ActionNewDay(); - void ActionNewWeek(); - void ActionNewWeekAIBonuses(); - void ActionNewMonth() const; void ActionPreBattle(); @@ -450,11 +447,6 @@ class AllCastles std::for_each( _castles.begin(), _castles.end(), []( Castle * castle ) { castle->ActionNewWeek(); } ); } - void NewWeekAI() - { - std::for_each( _castles.begin(), _castles.end(), []( Castle * castle ) { castle->ActionNewWeekAIBonuses(); } ); - } - void NewMonth() { std::for_each( _castles.begin(), _castles.end(), []( const Castle * castle ) { castle->ActionNewMonth(); } ); diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index f482ed746b1..4d5414ba1aa 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -109,11 +109,6 @@ double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) return 0; } -double Difficulty::GetUnitGrowthBonusForAI( const int /* difficulty */, const bool /* isCampaign */, const int /* race */, const building_t /* dwelling */ ) -{ - return 0; -} - int Difficulty::GetHeroMovementBonusForAI( int difficulty ) { switch ( difficulty ) { diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index ebdb7e6dd05..04a30d6e3be 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -52,9 +52,6 @@ namespace Difficulty // Returns an extra gold bonus modifier for AI based on difficulty level. This modifier is applied after applying the resource income bonus. double getGoldIncomeBonusForAI( const int difficulty ); - // Returns an extra growth bonus modifier for AI based on difficulty level. - double GetUnitGrowthBonusForAI( const int difficulty, const bool isCampaign, const int race, const building_t dwelling ); - int GetHeroMovementBonusForAI( int difficulty ); // Returns the ratio of the strength of the enemy army to the strength of the AI army, above which the AI decides to surrender or retreat from the battlefield diff --git a/src/fheroes2/game/game_startgame.cpp b/src/fheroes2/game/game_startgame.cpp index 95af5c3b021..8c6cdaef9f1 100644 --- a/src/fheroes2/game/game_startgame.cpp +++ b/src/fheroes2/game/game_startgame.cpp @@ -751,10 +751,6 @@ fheroes2::GameMode Interface::AdventureMap::StartGame() res = fheroes2::GameMode::END_TURN; - // All bonuses for AI must be applied on the first AI player turn, not the first player in general. - // This prevents human players from abusing AI bonuses. - bool applyAIBonuses = true; - for ( const Player * player : sortedPlayers ) { assert( player != nullptr ); @@ -830,12 +826,6 @@ fheroes2::GameMode Interface::AdventureMap::StartGame() // TODO: remove this temporary assertion assert( res == fheroes2::GameMode::END_TURN ); - if ( applyAIBonuses ) { - world.NewDayAI(); - - applyAIBonuses = false; - } - Cursor::Get().SetThemes( Cursor::WAIT ); conf.SetCurrentColor( playerColor ); diff --git a/src/fheroes2/world/world.cpp b/src/fheroes2/world/world.cpp index a778e300dcc..c046c5778f1 100644 --- a/src/fheroes2/world/world.cpp +++ b/src/fheroes2/world/world.cpp @@ -554,13 +554,6 @@ void World::NewDay() vec_eventsday.remove_if( [this]( const EventDate & v ) { return v.isDeprecated( day - 1 ); } ); } -void World::NewDayAI() -{ - if ( BeginWeek() ) { - vec_castles.NewWeekAI(); - } -} - void World::NewWeek() { // update objects diff --git a/src/fheroes2/world/world.h b/src/fheroes2/world/world.h index 60e85f3f9a0..16556ebd69e 100644 --- a/src/fheroes2/world/world.h +++ b/src/fheroes2/world/world.h @@ -307,7 +307,6 @@ class World : protected fheroes2::Size std::string DateString() const; void NewDay(); - void NewDayAI(); void NewWeek(); void NewMonth(); From 53c2ab0df93d1474c008f6cac3322ec887655b5f Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Wed, 13 Mar 2024 15:28:21 +0300 Subject: [PATCH 25/40] Update the copyright header --- src/fheroes2/world/world.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/world/world.h b/src/fheroes2/world/world.h index 16556ebd69e..ebd45dad858 100644 --- a/src/fheroes2/world/world.h +++ b/src/fheroes2/world/world.h @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2019 - 2023 * + * Copyright (C) 2019 - 2024 * * * * Free Heroes2 Engine: http://sourceforge.net/projects/fheroes2 * * Copyright (C) 2009 by Andrey Afletdinov * From 656b4f12478c3b4951fddc9881406ab27e95bb48 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Wed, 13 Mar 2024 15:36:00 +0300 Subject: [PATCH 26/40] Remove extra header --- src/fheroes2/castle/castle.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fheroes2/castle/castle.cpp b/src/fheroes2/castle/castle.cpp index 07357697ac4..97b534f338d 100644 --- a/src/fheroes2/castle/castle.cpp +++ b/src/fheroes2/castle/castle.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include From f5eaf29454ee6c2b8442cd313f647f617a95fc27 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 14 Mar 2024 01:31:54 +0300 Subject: [PATCH 27/40] Fully re-evaluate the castles status at the end of the AI turn --- src/fheroes2/ai/normal/ai_normal_kingdom.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp index db5dda86314..3aa97c823a5 100644 --- a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp +++ b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp @@ -901,6 +901,8 @@ namespace AI // Sync the list of castles (if new ones were captured during the turn) if ( castles.size() != sortedCastleList.size() ) { evaluateRegionSafety(); + + castlesInDanger = findCastlesInDanger( kingdom ); sortedCastleList = getSortedCastleList( castles, castlesInDanger ); } From f2b472b261e5640a97e27e4720bc994e7d4b71c8 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 14 Mar 2024 02:08:58 +0300 Subject: [PATCH 28/40] Style nits --- src/fheroes2/ai/normal/ai_normal_kingdom.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp index 3aa97c823a5..17277ef4091 100644 --- a/src/fheroes2/ai/normal/ai_normal_kingdom.cpp +++ b/src/fheroes2/ai/normal/ai_normal_kingdom.cpp @@ -480,12 +480,16 @@ namespace AI std::vector Normal::getSortedCastleList( const VecCastles & castles, const std::set & castlesInDanger ) { std::vector sortedCastleList; + sortedCastleList.reserve( castles.size() ); + for ( Castle * castle : castles ) { - if ( !castle ) + if ( castle == nullptr ) { continue; + } const int32_t castleIndex = castle->GetIndex(); const uint32_t regionID = world.GetTiles( castleIndex ).GetRegion(); + sortedCastleList.emplace_back( castle, castlesInDanger.count( castleIndex ) > 0, _regions[regionID].safetyFactor, castle->getBuildingValue() ); } @@ -569,7 +573,8 @@ namespace AI const uint32_t threatDistanceLimit = 3000; const int32_t castleIndex = castle.GetIndex(); - // skip precise distance check if army is too far to be a threat + + // Skip precise distance check if army is too far to be a threat if ( Maps::GetApproximateDistance( enemyArmy.index, castleIndex ) * Maps::Ground::roadPenalty > threatDistanceLimit ) { return false; } From e493c35b17505674029cc3cac677ed00190c6e8a Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 16 Mar 2024 11:54:59 +0300 Subject: [PATCH 29/40] Remove temporary variable --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index b8c6d188899..443cb84e7f9 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -616,15 +616,14 @@ namespace AI } ); }(); - const Force & force = arena.getForce( _myColor ); const Kingdom & kingdom = actualHero->GetKingdom(); - const bool isAbleToSurrender = [this, &arena, &force, &kingdom]() { + const bool isAbleToSurrender = [this, &arena, &kingdom]() { if ( !arena.CanSurrenderOpponent( _myColor ) ) { return false; } - return ( kingdom.AllowPayment( { Resource::GOLD, force.GetSurrenderCost() } ) ); + return ( kingdom.AllowPayment( { Resource::GOLD, arena.getForce( _myColor ).GetSurrenderCost() } ) ); }(); const bool isPossibleToReHire = [actualHero, &kingdom]() { From 1accb896218c65a43ca2bc48909422d2a5afb0e4 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 16 Mar 2024 12:02:09 +0300 Subject: [PATCH 30/40] Remove extra parentheses --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 443cb84e7f9..85b45c89b1e 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -623,7 +623,7 @@ namespace AI return false; } - return ( kingdom.AllowPayment( { Resource::GOLD, arena.getForce( _myColor ).GetSurrenderCost() } ) ); + return kingdom.AllowPayment( { Resource::GOLD, arena.getForce( _myColor ).GetSurrenderCost() } ); }(); const bool isPossibleToReHire = [actualHero, &kingdom]() { From 3b79d88127084192806ad272d217a633f0b50c0d Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 16 Mar 2024 16:31:41 +0300 Subject: [PATCH 31/40] Add castles-based bonus --- src/fheroes2/game/difficulty.cpp | 41 +++++++++++++++++++++++++++++--- src/fheroes2/game/difficulty.h | 2 +- src/fheroes2/kingdom/kingdom.cpp | 2 +- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 4d5414ba1aa..ae6cb44484e 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -26,6 +26,7 @@ #include #include "profit.h" +#include "race.h" #include "translations.h" std::string Difficulty::String( int difficulty ) @@ -66,7 +67,7 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) return 0; } -Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty ) +Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCastles & castles ) { const auto getIncomeFromSetsOfResourceMines = []( const uint32_t numOfSets ) { Funds result; @@ -76,13 +77,47 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty ) return result * numOfSets; }; + const auto getIncomeBasedOnCastles = [&castles]() { + Funds result; + + for ( const Castle * castle : castles ) { + assert( castle != nullptr ); + + switch ( castle->GetRace() ) { + case Race::KNGT: + result += { 0, 0, 0, 0, 1, 0, 0 }; // 1 unit of Crystal + break; + case Race::BARB: + result += { 0, 0, 0, 0, 1, 0, 0 }; // 1 unit of Crystal + break; + case Race::SORC: + result += { 0, 0, 1, 0, 0, 0, 0 }; // 1 unit of Mercury + break; + case Race::WRLK: + result += { 0, 0, 0, 1, 0, 0, 0 }; // 1 unit of Sulfur + break; + case Race::WZRD: + result += { 0, 0, 0, 0, 0, 1, 0 }; // 1 unit of Gems + break; + case Race::NECR: + result += { 0, 0, 0, 1, 0, 0, 0 }; // 1 unit of Sulfur + break; + default: + assert( 0 ); + break; + } + } + + return result; + }; + switch ( difficulty ) { case Difficulty::HARD: return getIncomeFromSetsOfResourceMines( 1 ); case Difficulty::EXPERT: - return getIncomeFromSetsOfResourceMines( 2 ); + return getIncomeFromSetsOfResourceMines( 2 ) + getIncomeBasedOnCastles(); case Difficulty::IMPOSSIBLE: - return getIncomeFromSetsOfResourceMines( 3 ); + return getIncomeFromSetsOfResourceMines( 3 ) + getIncomeBasedOnCastles(); default: break; } diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index 04a30d6e3be..3704c2b0974 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -47,7 +47,7 @@ namespace Difficulty int GetScoutingBonusForAI( int difficulty ); // Returns an extra resource bonus for AI based on difficulty level. - Funds getResourceIncomeBonusForAI( const int difficulty ); + Funds getResourceIncomeBonusForAI( const int difficulty, const VecCastles & castles ); // Returns an extra gold bonus modifier for AI based on difficulty level. This modifier is applied after applying the resource income bonus. double getGoldIncomeBonusForAI( const int difficulty ); diff --git a/src/fheroes2/kingdom/kingdom.cpp b/src/fheroes2/kingdom/kingdom.cpp index e3fdc9f37d6..e5dfc8c0fbb 100644 --- a/src/fheroes2/kingdom/kingdom.cpp +++ b/src/fheroes2/kingdom/kingdom.cpp @@ -707,7 +707,7 @@ Funds Kingdom::GetIncome( int type /* = INCOME_ALL */ ) const } if ( isControlAI() ) { - const Funds incomeBonus = Difficulty::getResourceIncomeBonusForAI( Game::getDifficulty() ); + const Funds incomeBonus = Difficulty::getResourceIncomeBonusForAI( Game::getDifficulty(), GetCastles() ); if ( incomeBonus.GetValidItemsCount() != 0 ) { DEBUG_LOG( DBG_AI, DBG_TRACE, "AI bonus to the resource income has been applied to " << Color::String( color ) << ": " << incomeBonus.String() ); From 54564ec7f2835043f5b91e508d87a90d1cfb5d0b Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 16 Mar 2024 16:39:48 +0300 Subject: [PATCH 32/40] Don't duplicate branches --- src/fheroes2/game/difficulty.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index ae6cb44484e..846f05ab92d 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -85,8 +85,6 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa switch ( castle->GetRace() ) { case Race::KNGT: - result += { 0, 0, 0, 0, 1, 0, 0 }; // 1 unit of Crystal - break; case Race::BARB: result += { 0, 0, 0, 0, 1, 0, 0 }; // 1 unit of Crystal break; @@ -94,14 +92,12 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa result += { 0, 0, 1, 0, 0, 0, 0 }; // 1 unit of Mercury break; case Race::WRLK: + case Race::NECR: result += { 0, 0, 0, 1, 0, 0, 0 }; // 1 unit of Sulfur break; case Race::WZRD: result += { 0, 0, 0, 0, 0, 1, 0 }; // 1 unit of Gems break; - case Race::NECR: - result += { 0, 0, 0, 1, 0, 0, 0 }; // 1 unit of Sulfur - break; default: assert( 0 ); break; From 4ec35f12280dc7ea77bafeedecd7b8ed86dd40af Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 16 Mar 2024 16:41:36 +0300 Subject: [PATCH 33/40] Apply IWYU suggestions --- src/fheroes2/game/difficulty.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 846f05ab92d..56d45bf38de 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -24,6 +24,7 @@ #include "difficulty.h" #include +#include #include "profit.h" #include "race.h" From 64fbcc85a24a34fba8f6d6f0550624578ea61674 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 16 Mar 2024 17:02:39 +0300 Subject: [PATCH 34/40] Rename the lambda --- src/fheroes2/game/difficulty.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 56d45bf38de..d305d3d0e5d 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -78,7 +78,7 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa return result * numOfSets; }; - const auto getIncomeBasedOnCastles = [&castles]() { + const auto getBonusForCastles = [&castles]() { Funds result; for ( const Castle * castle : castles ) { @@ -112,9 +112,9 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa case Difficulty::HARD: return getIncomeFromSetsOfResourceMines( 1 ); case Difficulty::EXPERT: - return getIncomeFromSetsOfResourceMines( 2 ) + getIncomeBasedOnCastles(); + return getIncomeFromSetsOfResourceMines( 2 ) + getBonusForCastles(); case Difficulty::IMPOSSIBLE: - return getIncomeFromSetsOfResourceMines( 3 ) + getIncomeBasedOnCastles(); + return getIncomeFromSetsOfResourceMines( 3 ) + getBonusForCastles(); default: break; } From 0b9987f25a0ced33f5fc4f6f7f6221f2f242f696 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Mon, 6 May 2024 22:52:57 +0300 Subject: [PATCH 35/40] Tune the bonus --- src/fheroes2/game/difficulty.cpp | 50 ++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index d305d3d0e5d..061cbf362f2 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -70,10 +70,10 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCastles & castles ) { - const auto getIncomeFromSetsOfResourceMines = []( const uint32_t numOfSets ) { + const auto getIncomeFromSetsOfResourceMines = []( const int resourceTypes, const uint32_t numOfSets ) { Funds result; - Resource::forEach( ( Resource::ALL & ~Resource::GOLD ), [&result]( const int res ) { result += ProfitConditions::FromMine( res ); } ); + Resource::forEach( resourceTypes, [&result]( const int res ) { result += ProfitConditions::FromMine( res ); } ); return result * numOfSets; }; @@ -84,20 +84,46 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa for ( const Castle * castle : castles ) { assert( castle != nullptr ); + // AI at higher difficulty levels should be able to fully redeem the weekly unit growth in its castles. It is necessary to give it the appropriate amount of + // bonus gold (some castles have more expensive units than others), as well as additional resources to redeem units of the maximum level. + switch ( castle->GetRace() ) { case Race::KNGT: case Race::BARB: - result += { 0, 0, 0, 0, 1, 0, 0 }; // 1 unit of Crystal + case Race::SORC: + case Race::NECR: + result += ProfitConditions::FromMine( Resource::GOLD ); + break; + case Race::WRLK: + case Race::WZRD: + result += ProfitConditions::FromMine( Resource::GOLD ) * 2; + break; + default: + assert( 0 ); + break; + } + + // Offer additional resources only if there are higher-level dwellings in the castle to avoid distortions in the castle's development rate + if ( !castle->isBuild( DWELLING_MONSTER6 ) ) { + continue; + } + + switch ( castle->GetRace() ) { + case Race::KNGT: + // The maximum level units in the knight's castle do not require resources for their recruitment + break; + case Race::BARB: + result += ProfitConditions::FromMine( Resource::CRYSTAL ); break; case Race::SORC: - result += { 0, 0, 1, 0, 0, 0, 0 }; // 1 unit of Mercury + result += ProfitConditions::FromMine( Resource::MERCURY ); break; case Race::WRLK: case Race::NECR: - result += { 0, 0, 0, 1, 0, 0, 0 }; // 1 unit of Sulfur + result += ProfitConditions::FromMine( Resource::SULFUR ); break; case Race::WZRD: - result += { 0, 0, 0, 0, 0, 1, 0 }; // 1 unit of Gems + result += ProfitConditions::FromMine( Resource::GEMS ); break; default: assert( 0 ); @@ -110,11 +136,11 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa switch ( difficulty ) { case Difficulty::HARD: - return getIncomeFromSetsOfResourceMines( 1 ); + return getIncomeFromSetsOfResourceMines( Resource::ORE | Resource::WOOD | Resource::GOLD, 1 ); case Difficulty::EXPERT: - return getIncomeFromSetsOfResourceMines( 2 ) + getBonusForCastles(); + return getIncomeFromSetsOfResourceMines( Resource::ORE | Resource::WOOD | Resource::GOLD, 1 ) + getBonusForCastles(); case Difficulty::IMPOSSIBLE: - return getIncomeFromSetsOfResourceMines( 3 ) + getBonusForCastles(); + return getIncomeFromSetsOfResourceMines( Resource::ALL, 1 ) + getBonusForCastles(); default: break; } @@ -128,12 +154,6 @@ double Difficulty::getGoldIncomeBonusForAI( const int difficulty ) case Difficulty::EASY: // It is deduction from the income. return -0.25; - case Difficulty::HARD: - return 1.0; - case Difficulty::EXPERT: - return 2.0; - case Difficulty::IMPOSSIBLE: - return 3.0; default: break; } From a48b0c32d1bb8441e2c6dbc966b342801827c254 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Tue, 7 May 2024 00:22:13 +0300 Subject: [PATCH 36/40] Bone Dragons doesn't require resources to hire them --- src/fheroes2/game/difficulty.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 061cbf362f2..4e91e8f617b 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -119,12 +119,14 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa result += ProfitConditions::FromMine( Resource::MERCURY ); break; case Race::WRLK: - case Race::NECR: result += ProfitConditions::FromMine( Resource::SULFUR ); break; case Race::WZRD: result += ProfitConditions::FromMine( Resource::GEMS ); break; + case Race::NECR: + // The maximum level units in the necromancer's castle do not require resources for their recruitment + break; default: assert( 0 ); break; From c4867ba3177d706f7016df0ec99ccaf1e512c6b9 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Tue, 7 May 2024 11:44:03 +0300 Subject: [PATCH 37/40] Give the additional gold to Wizard & Warlock only if they have the level 6 dwelling --- src/fheroes2/game/difficulty.cpp | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index 4e91e8f617b..cd28051b47e 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -84,24 +84,8 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa for ( const Castle * castle : castles ) { assert( castle != nullptr ); - // AI at higher difficulty levels should be able to fully redeem the weekly unit growth in its castles. It is necessary to give it the appropriate amount of - // bonus gold (some castles have more expensive units than others), as well as additional resources to redeem units of the maximum level. - - switch ( castle->GetRace() ) { - case Race::KNGT: - case Race::BARB: - case Race::SORC: - case Race::NECR: - result += ProfitConditions::FromMine( Resource::GOLD ); - break; - case Race::WRLK: - case Race::WZRD: - result += ProfitConditions::FromMine( Resource::GOLD ) * 2; - break; - default: - assert( 0 ); - break; - } + // AI at higher difficulty levels should be able to fully redeem the weekly unit growth in its castles + result += ProfitConditions::FromMine( Resource::GOLD ); // Offer additional resources only if there are higher-level dwellings in the castle to avoid distortions in the castle's development rate if ( !castle->isBuild( DWELLING_MONSTER6 ) ) { @@ -110,7 +94,8 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa switch ( castle->GetRace() ) { case Race::KNGT: - // The maximum level units in the knight's castle do not require resources for their recruitment + case Race::NECR: + // Rare resources are not required to hire maximum-level units in these castles break; case Race::BARB: result += ProfitConditions::FromMine( Resource::CRYSTAL ); @@ -120,12 +105,13 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa break; case Race::WRLK: result += ProfitConditions::FromMine( Resource::SULFUR ); + // The maximum level units in this castle are more expensive than in others + result += ProfitConditions::FromMine( Resource::GOLD ); break; case Race::WZRD: result += ProfitConditions::FromMine( Resource::GEMS ); - break; - case Race::NECR: - // The maximum level units in the necromancer's castle do not require resources for their recruitment + // The maximum level units in this castle are more expensive than in others + result += ProfitConditions::FromMine( Resource::GOLD ); break; default: assert( 0 ); From 2ca241b30066572a4fee6b666a1d55726ee8b8be Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Thu, 16 May 2024 00:21:02 +0300 Subject: [PATCH 38/40] Apply IWYU suggestions --- src/fheroes2/ai/normal/ai_normal_battle.cpp | 1 + src/fheroes2/ai/normal/ai_normal_hero.cpp | 1 + src/fheroes2/battle/battle_grave.cpp | 1 + src/fheroes2/battle/battle_interface.cpp | 1 + src/fheroes2/battle/battle_pathfinding.cpp | 1 + src/fheroes2/world/world.cpp | 1 + 6 files changed, 6 insertions(+) diff --git a/src/fheroes2/ai/normal/ai_normal_battle.cpp b/src/fheroes2/ai/normal/ai_normal_battle.cpp index 85b45c89b1e..94cc80504f6 100644 --- a/src/fheroes2/ai/normal/ai_normal_battle.cpp +++ b/src/fheroes2/ai/normal/ai_normal_battle.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include diff --git a/src/fheroes2/ai/normal/ai_normal_hero.cpp b/src/fheroes2/ai/normal/ai_normal_hero.cpp index 787d115a1b7..0ab88957ec7 100644 --- a/src/fheroes2/ai/normal/ai_normal_hero.cpp +++ b/src/fheroes2/ai/normal/ai_normal_hero.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include diff --git a/src/fheroes2/battle/battle_grave.cpp b/src/fheroes2/battle/battle_grave.cpp index 7ce18896796..2288e176d54 100644 --- a/src/fheroes2/battle/battle_grave.cpp +++ b/src/fheroes2/battle/battle_grave.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include "battle_board.h" diff --git a/src/fheroes2/battle/battle_interface.cpp b/src/fheroes2/battle/battle_interface.cpp index 88bf0ea4157..dcfc8cce788 100644 --- a/src/fheroes2/battle/battle_interface.cpp +++ b/src/fheroes2/battle/battle_interface.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include "agg_image.h" #include "audio.h" diff --git a/src/fheroes2/battle/battle_pathfinding.cpp b/src/fheroes2/battle/battle_pathfinding.cpp index 55f773e5f28..5bd26e53a0a 100644 --- a/src/fheroes2/battle/battle_pathfinding.cpp +++ b/src/fheroes2/battle/battle_pathfinding.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "battle_arena.h" diff --git a/src/fheroes2/world/world.cpp b/src/fheroes2/world/world.cpp index 91fa9bb6670..2334b220312 100644 --- a/src/fheroes2/world/world.cpp +++ b/src/fheroes2/world/world.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include "ai.h" From 0633eab545043f1dfabce394d37c80abf5b13fa3 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Fri, 31 May 2024 16:12:51 +0300 Subject: [PATCH 39/40] Tune the bonuses --- src/fheroes2/game/difficulty.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index cd28051b47e..f3f3e06a504 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -124,11 +124,11 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa switch ( difficulty ) { case Difficulty::HARD: - return getIncomeFromSetsOfResourceMines( Resource::ORE | Resource::WOOD | Resource::GOLD, 1 ); + return getIncomeFromSetsOfResourceMines( Resource::GOLD, 1 ); case Difficulty::EXPERT: - return getIncomeFromSetsOfResourceMines( Resource::ORE | Resource::WOOD | Resource::GOLD, 1 ) + getBonusForCastles(); + return getIncomeFromSetsOfResourceMines( Resource::GOLD, 1 ) + getBonusForCastles(); case Difficulty::IMPOSSIBLE: - return getIncomeFromSetsOfResourceMines( Resource::ALL, 1 ) + getBonusForCastles(); + return getIncomeFromSetsOfResourceMines( Resource::GOLD, 2 ) + getBonusForCastles(); default: break; } From 10e41fa08f6ef9c72c2878344db08604984ae8a2 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sat, 1 Jun 2024 16:32:21 +0300 Subject: [PATCH 40/40] Tune the bonuses, update the logic a bit --- src/fheroes2/castle/castle.cpp | 13 ++++---- src/fheroes2/castle/castle.h | 9 ++++-- src/fheroes2/game/difficulty.cpp | 41 ++++++++++++++++++++----- src/fheroes2/game/difficulty.h | 4 ++- src/fheroes2/kingdom/kingdom.cpp | 51 +++++++++++++++++++------------- 5 files changed, 78 insertions(+), 40 deletions(-) diff --git a/src/fheroes2/castle/castle.cpp b/src/fheroes2/castle/castle.cpp index 431ac749182..f8f3cbfda9d 100644 --- a/src/fheroes2/castle/castle.cpp +++ b/src/fheroes2/castle/castle.cpp @@ -2414,15 +2414,12 @@ void Castle::ActionAfterBattle( bool attacker_wins ) Castle * VecCastles::GetFirstCastle() const { - const_iterator it = std::find_if( begin(), end(), []( const Castle * castle ) { return castle->isCastle(); } ); - return end() != it ? *it : nullptr; -} + const_iterator iter = std::find_if( begin(), end(), []( const Castle * castle ) { return castle->isCastle(); } ); + if ( iter == end() ) { + return nullptr; + } -void VecCastles::ChangeColors( int col1, int col2 ) -{ - for ( iterator it = begin(); it != end(); ++it ) - if ( ( *it )->GetColor() == col1 ) - ( *it )->ChangeColor( col2 ); + return *iter; } AllCastles::AllCastles() diff --git a/src/fheroes2/castle/castle.h b/src/fheroes2/castle/castle.h index 38e94737af7..c163b0c64d7 100644 --- a/src/fheroes2/castle/castle.h +++ b/src/fheroes2/castle/castle.h @@ -413,9 +413,14 @@ namespace CastleDialog struct VecCastles : public std::vector { - Castle * GetFirstCastle() const; + VecCastles() = default; + VecCastles( const VecCastles & ) = delete; + + ~VecCastles() = default; - void ChangeColors( int, int ); + VecCastles & operator=( const VecCastles & ) = delete; + + Castle * GetFirstCastle() const; }; class AllCastles diff --git a/src/fheroes2/game/difficulty.cpp b/src/fheroes2/game/difficulty.cpp index f3f3e06a504..b2d3bbfb8df 100644 --- a/src/fheroes2/game/difficulty.cpp +++ b/src/fheroes2/game/difficulty.cpp @@ -23,12 +23,15 @@ #include "difficulty.h" +#include #include #include +#include "kingdom.h" #include "profit.h" #include "race.h" #include "translations.h" +#include "world.h" std::string Difficulty::String( int difficulty ) { @@ -68,8 +71,10 @@ int Difficulty::GetScoutingBonusForAI( int difficulty ) return 0; } -Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCastles & castles ) +Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const Kingdom & kingdom ) { + assert( kingdom.isControlAI() ); + const auto getIncomeFromSetsOfResourceMines = []( const int resourceTypes, const uint32_t numOfSets ) { Funds result; @@ -78,16 +83,28 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa return result * numOfSets; }; - const auto getBonusForCastles = [&castles]() { + const auto getBonusForCastles = [kingdomColor = kingdom.GetColor(), &kingdomCastles = kingdom.GetCastles()]() { Funds result; - for ( const Castle * castle : castles ) { + const bool kingdomHasMarketplace = std::any_of( kingdomCastles.begin(), kingdomCastles.end(), []( const Castle * castle ) { + assert( castle != nullptr ); + + return castle->isBuild( BUILD_MARKETPLACE ); + } ); + + // Additional rare resources for hiring units from higher-level dwellings can only be provided if the kingdom already has some source of those resources - either + // through trade or through mining + const auto doesKingdomHaveResourceSource = [kingdomColor, kingdomHasMarketplace]( const int resourceType ) { + return ( kingdomHasMarketplace || world.CountCapturedMines( resourceType, kingdomColor ) > 0 ); + }; + + for ( const Castle * castle : kingdomCastles ) { assert( castle != nullptr ); // AI at higher difficulty levels should be able to fully redeem the weekly unit growth in its castles result += ProfitConditions::FromMine( Resource::GOLD ); - // Offer additional resources only if there are higher-level dwellings in the castle to avoid distortions in the castle's development rate + // Provide additional resources only if there are higher-level dwellings in the castle to avoid distortions in the castle's development rate if ( !castle->isBuild( DWELLING_MONSTER6 ) ) { continue; } @@ -98,18 +115,26 @@ Funds Difficulty::getResourceIncomeBonusForAI( const int difficulty, const VecCa // Rare resources are not required to hire maximum-level units in these castles break; case Race::BARB: - result += ProfitConditions::FromMine( Resource::CRYSTAL ); + if ( doesKingdomHaveResourceSource( Resource::CRYSTAL ) ) { + result += ProfitConditions::FromMine( Resource::CRYSTAL ); + } break; case Race::SORC: - result += ProfitConditions::FromMine( Resource::MERCURY ); + if ( doesKingdomHaveResourceSource( Resource::MERCURY ) ) { + result += ProfitConditions::FromMine( Resource::MERCURY ); + } break; case Race::WRLK: - result += ProfitConditions::FromMine( Resource::SULFUR ); + if ( doesKingdomHaveResourceSource( Resource::SULFUR ) ) { + result += ProfitConditions::FromMine( Resource::SULFUR ); + } // The maximum level units in this castle are more expensive than in others result += ProfitConditions::FromMine( Resource::GOLD ); break; case Race::WZRD: - result += ProfitConditions::FromMine( Resource::GEMS ); + if ( doesKingdomHaveResourceSource( Resource::GEMS ) ) { + result += ProfitConditions::FromMine( Resource::GEMS ); + } // The maximum level units in this castle are more expensive than in others result += ProfitConditions::FromMine( Resource::GOLD ); break; diff --git a/src/fheroes2/game/difficulty.h b/src/fheroes2/game/difficulty.h index 3704c2b0974..93daa9967f1 100644 --- a/src/fheroes2/game/difficulty.h +++ b/src/fheroes2/game/difficulty.h @@ -29,6 +29,8 @@ #include "castle.h" #include "resource.h" +class Kingdom; + namespace Difficulty { // !!! IMPORTANT !!! @@ -47,7 +49,7 @@ namespace Difficulty int GetScoutingBonusForAI( int difficulty ); // Returns an extra resource bonus for AI based on difficulty level. - Funds getResourceIncomeBonusForAI( const int difficulty, const VecCastles & castles ); + Funds getResourceIncomeBonusForAI( const int difficulty, const Kingdom & kingdom ); // Returns an extra gold bonus modifier for AI based on difficulty level. This modifier is applied after applying the resource income bonus. double getGoldIncomeBonusForAI( const int difficulty ); diff --git a/src/fheroes2/kingdom/kingdom.cpp b/src/fheroes2/kingdom/kingdom.cpp index c7d2e000242..27f25f88611 100644 --- a/src/fheroes2/kingdom/kingdom.cpp +++ b/src/fheroes2/kingdom/kingdom.cpp @@ -178,25 +178,30 @@ bool Kingdom::isPlay() const void Kingdom::LossPostActions() { - if ( isPlay() ) { - Players::SetPlayerInGame( color, false ); + if ( !isPlay() ) { + return; + } - // Heroes::Dismiss() calls Kingdom::RemoveHero(), which eventually calls heroes.erase() - while ( !heroes.empty() ) { - Heroes * hero = heroes.back(); + Players::SetPlayerInGame( color, false ); - assert( hero->GetColor() == GetColor() ); + // Heroes::Dismiss() calls Kingdom::RemoveHero(), which eventually calls heroes.erase() + while ( !heroes.empty() ) { + Heroes * hero = heroes.back(); - hero->Dismiss( static_cast( Battle::RESULT_LOSS ) ); - } + assert( hero->GetColor() == GetColor() ); - if ( !castles.empty() ) { - castles.ChangeColors( GetColor(), Color::NONE ); - castles.clear(); - } + hero->Dismiss( static_cast( Battle::RESULT_LOSS ) ); + } + + for ( Castle * castle : castles ) { + assert( castle != nullptr && castle->GetColor() == GetColor() ); - world.ResetCapturedObjects( GetColor() ); + castle->ChangeColor( Color::NONE ); } + + castles.clear(); + + world.ResetCapturedObjects( GetColor() ); } void Kingdom::ActionBeforeTurn() @@ -598,20 +603,22 @@ bool Kingdom::AllowRecruitHero( bool check_payment ) const void Kingdom::ApplyPlayWithStartingHero() { - if ( !isPlay() || castles.empty() ) + if ( !isPlay() || castles.empty() ) { return; + } bool foundHeroes = false; for ( const Castle * castle : castles ) { - if ( castle == nullptr ) + if ( castle == nullptr ) { continue; + } - // check manual set hero (castle position + point(0, 1))? + // Check if there is a hero placed by the map creator near the castle entrance (castle position + point(0, 1)) const fheroes2::Point & cp = castle->GetCenter(); Heroes * hero = world.GetTiles( cp.x, cp.y + 1 ).getHero(); - // and move manual set hero to castle + // If there is, move it to the castle if ( hero && hero->GetColor() == GetColor() ) { const bool patrol = hero->Modes( Heroes::PATROL ); if ( hero->isValid() ) { @@ -626,19 +633,21 @@ void Kingdom::ApplyPlayWithStartingHero() hero->SetModes( Heroes::PATROL ); hero->SetPatrolCenter( cp ); } + foundHeroes = true; } } if ( !foundHeroes && Settings::Get().getCurrentMapInfo().startWithHeroInEachCastle ) { - // get first castle const Castle * first = castles.GetFirstCastle(); - if ( nullptr == first ) + if ( first == nullptr ) { first = castles.front(); + } Heroes * hero = world.GetHeroForHire( first->GetRace() ); - if ( hero && AllowRecruitHero( false ) ) + if ( hero && AllowRecruitHero( false ) ) { hero->Recruit( *first ); + } } } @@ -707,7 +716,7 @@ Funds Kingdom::GetIncome( int type /* = INCOME_ALL */ ) const } if ( isControlAI() ) { - const Funds incomeBonus = Difficulty::getResourceIncomeBonusForAI( Game::getDifficulty(), GetCastles() ); + const Funds incomeBonus = Difficulty::getResourceIncomeBonusForAI( Game::getDifficulty(), *this ); if ( incomeBonus.GetValidItemsCount() != 0 ) { DEBUG_LOG( DBG_AI, DBG_TRACE, "AI bonus to the resource income has been applied to " << Color::String( color ) << ": " << incomeBonus.String() );