From f431b29cafe2acfb3abecff3eb5a6db4903de9c1 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 9 Mar 2025 16:49:04 +0800 Subject: [PATCH 1/9] Implement Dragon Slayer spell selection for the AI --- src/fheroes2/ai/ai_battle.h | 1 + src/fheroes2/ai/ai_battle_spell.cpp | 51 ++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/fheroes2/ai/ai_battle.h b/src/fheroes2/ai/ai_battle.h index 50dab5c2b2..efaefecac2 100644 --- a/src/fheroes2/ai/ai_battle.h +++ b/src/fheroes2/ai/ai_battle.h @@ -103,6 +103,7 @@ namespace AI SpellcastOutcome spellDispelValue( const Spell & spell, const Battle::Units & friendly, const Battle::Units & enemies ) const; SpellcastOutcome spellResurrectValue( const Spell & spell, const Battle::Arena & arena ) const; SpellcastOutcome spellSummonValue( const Spell & spell, const Battle::Arena & arena, const int heroColor ) const; + SpellcastOutcome spellDragonSlayerValue( const Spell & spell, const Battle::Units & friendly, const Battle::Units & enemies ) const; SpellcastOutcome spellEffectValue( const Spell & spell, const Battle::Units & targets ) const; double spellEffectValue( const Spell & spell, const Battle::Unit & target, bool targetIsLast, bool forDispel ) const; diff --git a/src/fheroes2/ai/ai_battle_spell.cpp b/src/fheroes2/ai/ai_battle_spell.cpp index c870f00b4c..84e3fa212c 100644 --- a/src/fheroes2/ai/ai_battle_spell.cpp +++ b/src/fheroes2/ai/ai_battle_spell.cpp @@ -47,6 +47,8 @@ namespace { const double antimagicLowLimit = 200.0; + const double bloodLustRatio = 0.1; + double ReduceEffectivenessByDistance( const Battle::Unit & unit ) { // Reduce spell effectiveness if unit already crossed the battlefield @@ -184,6 +186,9 @@ AI::SpellSelection AI::BattlePlanner::selectBestSpell( Battle::Arena & arena, co else if ( spell.isResurrect() ) { checkSelectBestSpell( spell, spellResurrectValue( spell, arena ) ); } + else if ( spell == Spell::DRAGONSLAYER ) { + checkSelectBestSpell( spell, spellDragonSlayerValue( spell, friendly, enemies ) ); + } else if ( spell.isApplyToFriends() ) { checkSelectBestSpell( spell, spellEffectValue( spell, trueFriendly ) ); } @@ -434,7 +439,7 @@ double AI::BattlePlanner::spellEffectValue( const Spell & spell, const Battle::U ratio = getSpellHasteRatio( target ); break; case Spell::BLOODLUST: - ratio = 0.1; + ratio = bloodLustRatio; break; case Spell::BLESS: case Spell::MASSBLESS: { @@ -450,7 +455,6 @@ double AI::BattlePlanner::spellEffectValue( const Spell & spell, const Battle::U ratio = 0.2; break; // Following spell usefulness is conditional; ratio will be determined later - case Spell::DRAGONSLAYER: case Spell::ANTIMAGIC: case Spell::MIRRORIMAGE: case Spell::SHIELD: @@ -514,9 +518,6 @@ double AI::BattlePlanner::spellEffectValue( const Spell & spell, const Battle::U ratio *= 1.25; } } - else if ( spellID == Spell::DRAGONSLAYER ) { - // TODO: add logic to check if the enemy army contains a dragon. - } return target.GetStrength() * ratio * spellDurationMultiplier( target ); } @@ -657,3 +658,43 @@ AI::SpellcastOutcome AI::BattlePlanner::spellSummonValue( const Spell & spell, c return bestOutcome; } + +AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & spell, const Battle::Units & friendly, const Battle::Units & enemies ) const +{ + assert( spell.GetID() == Spell::DRAGONSLAYER ); + + size_t enemyDragons = 0; + + // Check whether the enemy army has any Dragons. + for ( const Battle::Unit * unit : enemies ) { + if ( unit->isDragons() ) { + ++enemyDragons; + } + } + + if ( enemyDragons == 0 ) { + // This spell is useless as no Dragons exist in the enemy army. + return {}; + } + + // Make an estimation based on the value of Blood Lust since this spell also increases Attack but against every creature. + // If the enemy army consists of other monsters that are not Dragons then Dragon Slayer spell isn't that valuable anymore. + const double bloodLustAttackIncrease = Spell( Spell::BLOODLUST ).ExtraValue(); + const double dragonSlayerAttackIncrease = spell.ExtraValue(); + + const double dragonSlayerRatio = bloodLustRatio * dragonSlayerAttackIncrease / bloodLustAttackIncrease * enemyDragons / enemies.size(); + + SpellcastOutcome bestOutcome; + + for ( const Battle::Unit * unit : friendly ) { + if ( isSpellcastUselessForUnit( *unit, spell ) ) { + continue; + } + + double unitValue = unit->GetStrength() * dragonSlayerRatio * spellDurationMultiplier( *unit ); + + bestOutcome.updateOutcome( unitValue, unit->GetHeadIndex(), false ); + } + + return bestOutcome; +} From 57b760f219f21ccc1a8150161a0cdf6d8ccf969f Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 9 Mar 2025 16:50:14 +0800 Subject: [PATCH 2/9] Fix copyright year --- src/fheroes2/ai/ai_battle_spell.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/ai/ai_battle_spell.cpp b/src/fheroes2/ai/ai_battle_spell.cpp index 84e3fa212c..3948ac124f 100644 --- a/src/fheroes2/ai/ai_battle_spell.cpp +++ b/src/fheroes2/ai/ai_battle_spell.cpp @@ -1,6 +1,6 @@ /*************************************************************************** * fheroes2: https://github.com/ihhub/fheroes2 * - * Copyright (C) 2024 * + * Copyright (C) 2024 - 2025 * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * From 109505dcfb09362235bdb41c041a89b1effd6c3e Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Sun, 9 Mar 2025 16:54:37 +0800 Subject: [PATCH 3/9] Fix clang-tidy complaints --- src/fheroes2/ai/ai_battle_spell.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/fheroes2/ai/ai_battle_spell.cpp b/src/fheroes2/ai/ai_battle_spell.cpp index 3948ac124f..b17ae852b5 100644 --- a/src/fheroes2/ai/ai_battle_spell.cpp +++ b/src/fheroes2/ai/ai_battle_spell.cpp @@ -663,7 +663,7 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp { assert( spell.GetID() == Spell::DRAGONSLAYER ); - size_t enemyDragons = 0; + int32_t enemyDragons = 0; // Check whether the enemy army has any Dragons. for ( const Battle::Unit * unit : enemies ) { @@ -682,7 +682,7 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp const double bloodLustAttackIncrease = Spell( Spell::BLOODLUST ).ExtraValue(); const double dragonSlayerAttackIncrease = spell.ExtraValue(); - const double dragonSlayerRatio = bloodLustRatio * dragonSlayerAttackIncrease / bloodLustAttackIncrease * enemyDragons / enemies.size(); + const double dragonSlayerRatio = bloodLustRatio * dragonSlayerAttackIncrease / bloodLustAttackIncrease * enemyDragons / static_cast( enemies.size() ); SpellcastOutcome bestOutcome; @@ -691,8 +691,7 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp continue; } - double unitValue = unit->GetStrength() * dragonSlayerRatio * spellDurationMultiplier( *unit ); - + const double unitValue = unit->GetStrength() * dragonSlayerRatio * spellDurationMultiplier( *unit ); bestOutcome.updateOutcome( unitValue, unit->GetHeadIndex(), false ); } From 9649bf8db3fc6650aa568f7133921c6839621650 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sun, 9 Mar 2025 23:07:27 +0300 Subject: [PATCH 4/9] Propagate the const --- src/fheroes2/ai/ai_battle_spell.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/fheroes2/ai/ai_battle_spell.cpp b/src/fheroes2/ai/ai_battle_spell.cpp index b17ae852b5..e6c1596eb8 100644 --- a/src/fheroes2/ai/ai_battle_spell.cpp +++ b/src/fheroes2/ai/ai_battle_spell.cpp @@ -663,16 +663,14 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp { assert( spell.GetID() == Spell::DRAGONSLAYER ); - int32_t enemyDragons = 0; - // Check whether the enemy army has any Dragons. - for ( const Battle::Unit * unit : enemies ) { - if ( unit->isDragons() ) { - ++enemyDragons; - } - } + const auto numOfSlotsWithEnemyDragons = std::count_if( enemies.begin(), enemies.end(), []( const Battle::Unit * unit ) { + assert( unit != nullptr ); + + return unit->isDragons(); + } ); - if ( enemyDragons == 0 ) { + if ( numOfSlotsWithEnemyDragons == 0 ) { // This spell is useless as no Dragons exist in the enemy army. return {}; } @@ -682,7 +680,8 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp const double bloodLustAttackIncrease = Spell( Spell::BLOODLUST ).ExtraValue(); const double dragonSlayerAttackIncrease = spell.ExtraValue(); - const double dragonSlayerRatio = bloodLustRatio * dragonSlayerAttackIncrease / bloodLustAttackIncrease * enemyDragons / static_cast( enemies.size() ); + const double dragonSlayerRatio + = bloodLustRatio * dragonSlayerAttackIncrease / bloodLustAttackIncrease * numOfSlotsWithEnemyDragons / static_cast( enemies.size() ); SpellcastOutcome bestOutcome; From 1146ec30c8ee72fa0960ca036be1b39739359a84 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Sun, 9 Mar 2025 23:17:53 +0300 Subject: [PATCH 5/9] Add static_cast --- src/fheroes2/ai/ai_battle_spell.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fheroes2/ai/ai_battle_spell.cpp b/src/fheroes2/ai/ai_battle_spell.cpp index e6c1596eb8..e0ac34f856 100644 --- a/src/fheroes2/ai/ai_battle_spell.cpp +++ b/src/fheroes2/ai/ai_battle_spell.cpp @@ -680,8 +680,8 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp const double bloodLustAttackIncrease = Spell( Spell::BLOODLUST ).ExtraValue(); const double dragonSlayerAttackIncrease = spell.ExtraValue(); - const double dragonSlayerRatio - = bloodLustRatio * dragonSlayerAttackIncrease / bloodLustAttackIncrease * numOfSlotsWithEnemyDragons / static_cast( enemies.size() ); + const double dragonSlayerRatio = bloodLustRatio * dragonSlayerAttackIncrease / bloodLustAttackIncrease * static_cast( numOfSlotsWithEnemyDragons ) + / static_cast( enemies.size() ); SpellcastOutcome bestOutcome; From b21e28c1aa34972d31c5a884c963b9c03e8a3f09 Mon Sep 17 00:00:00 2001 From: Oleg Derevenetz Date: Mon, 10 Mar 2025 00:13:54 +0300 Subject: [PATCH 6/9] Add assertions --- src/fheroes2/ai/ai_battle_spell.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/fheroes2/ai/ai_battle_spell.cpp b/src/fheroes2/ai/ai_battle_spell.cpp index e0ac34f856..c7237e83fd 100644 --- a/src/fheroes2/ai/ai_battle_spell.cpp +++ b/src/fheroes2/ai/ai_battle_spell.cpp @@ -675,13 +675,20 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp return {}; } - // Make an estimation based on the value of Blood Lust since this spell also increases Attack but against every creature. + const auto getSpellAttackBonus = []( const Spell & sp ) { + const uint32_t bonus = sp.ExtraValue(); + assert( bonus > 0 ); + + return bonus; + }; + + // Make an estimation based on the value of Bloodlust since this spell also increases Attack but against every creature. // If the enemy army consists of other monsters that are not Dragons then Dragon Slayer spell isn't that valuable anymore. - const double bloodLustAttackIncrease = Spell( Spell::BLOODLUST ).ExtraValue(); - const double dragonSlayerAttackIncrease = spell.ExtraValue(); + const double bloodlustAttackBonus = getSpellAttackBonus( Spell::BLOODLUST ); + const double dragonSlayerAttackBonus = getSpellAttackBonus( spell ); - const double dragonSlayerRatio = bloodLustRatio * dragonSlayerAttackIncrease / bloodLustAttackIncrease * static_cast( numOfSlotsWithEnemyDragons ) - / static_cast( enemies.size() ); + const double dragonSlayerRatio + = bloodLustRatio * dragonSlayerAttackBonus / bloodlustAttackBonus * static_cast( numOfSlotsWithEnemyDragons ) / static_cast( enemies.size() ); SpellcastOutcome bestOutcome; From 3ec484d7a26fc109eb0e0ac2ac7ffbfcdb73a01f Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 12 Mar 2025 22:01:05 +0800 Subject: [PATCH 7/9] Address comment --- src/fheroes2/ai/ai_battle_spell.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/fheroes2/ai/ai_battle_spell.cpp b/src/fheroes2/ai/ai_battle_spell.cpp index c7237e83fd..352ff96b62 100644 --- a/src/fheroes2/ai/ai_battle_spell.cpp +++ b/src/fheroes2/ai/ai_battle_spell.cpp @@ -663,12 +663,23 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp { assert( spell.GetID() == Spell::DRAGONSLAYER ); - // Check whether the enemy army has any Dragons. - const auto numOfSlotsWithEnemyDragons = std::count_if( enemies.begin(), enemies.end(), []( const Battle::Unit * unit ) { + double enemyArmyStrength = 0; + double dragonsStrength = 0; + + size_t numOfSlotsWithEnemyDragons = 0; + + for ( const Battle::Unit * unit : enemies ) { assert( unit != nullptr ); - return unit->isDragons(); - } ); + const double strength = unit->GetStrength(); + + if ( unit->isDragons() ) { + ++numOfSlotsWithEnemyDragons; + dragonsStrength += strength; + } + + enemyArmyStrength += strength; + } if ( numOfSlotsWithEnemyDragons == 0 ) { // This spell is useless as no Dragons exist in the enemy army. @@ -687,8 +698,7 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp const double bloodlustAttackBonus = getSpellAttackBonus( Spell::BLOODLUST ); const double dragonSlayerAttackBonus = getSpellAttackBonus( spell ); - const double dragonSlayerRatio - = bloodLustRatio * dragonSlayerAttackBonus / bloodlustAttackBonus * static_cast( numOfSlotsWithEnemyDragons ) / static_cast( enemies.size() ); + const double dragonSlayerRatio = bloodLustRatio * dragonSlayerAttackBonus / bloodlustAttackBonus * dragonsStrength / enemyArmyStrength; SpellcastOutcome bestOutcome; From 17bdcdc9143cb9b69a1221143105f4b26310df97 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 12 Mar 2025 22:06:26 +0800 Subject: [PATCH 8/9] Use int32_t for count --- src/fheroes2/ai/ai_battle_spell.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/ai/ai_battle_spell.cpp b/src/fheroes2/ai/ai_battle_spell.cpp index 352ff96b62..3fcd9d5f28 100644 --- a/src/fheroes2/ai/ai_battle_spell.cpp +++ b/src/fheroes2/ai/ai_battle_spell.cpp @@ -666,7 +666,7 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp double enemyArmyStrength = 0; double dragonsStrength = 0; - size_t numOfSlotsWithEnemyDragons = 0; + int32_t numOfSlotsWithEnemyDragons = 0; for ( const Battle::Unit * unit : enemies ) { assert( unit != nullptr ); From d59c23e0c16ac4072601de33442d5670218b0c16 Mon Sep 17 00:00:00 2001 From: Ihar Hubchyk Date: Wed, 12 Mar 2025 22:13:12 +0800 Subject: [PATCH 9/9] Update src/fheroes2/ai/ai_battle_spell.cpp Co-authored-by: Oleg Derevenetz --- src/fheroes2/ai/ai_battle_spell.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fheroes2/ai/ai_battle_spell.cpp b/src/fheroes2/ai/ai_battle_spell.cpp index 3fcd9d5f28..64b8ca1ef6 100644 --- a/src/fheroes2/ai/ai_battle_spell.cpp +++ b/src/fheroes2/ai/ai_battle_spell.cpp @@ -669,7 +669,7 @@ AI::SpellcastOutcome AI::BattlePlanner::spellDragonSlayerValue( const Spell & sp int32_t numOfSlotsWithEnemyDragons = 0; for ( const Battle::Unit * unit : enemies ) { - assert( unit != nullptr ); + assert( unit != nullptr && unit->isValid() ); const double strength = unit->GetStrength();