Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AI should determine best skill when leveling up heroes #8591

Merged
merged 12 commits into from
Jul 29, 2024
3 changes: 3 additions & 0 deletions src/fheroes2/ai/ai_planner.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "mp2.h"
#include "pairs.h"
#include "resource.h"
#include "skill.h"
#include "world_pathfinding.h"

class Castle;
Expand Down Expand Up @@ -177,6 +178,8 @@ namespace AI
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 );

Skill::Secondary pickSecondarySkill( const Heroes & hero, const Skill::Secondary & left, const Skill::Secondary & right );
oleg-derevenetz marked this conversation as resolved.
Show resolved Hide resolved

static void HeroesPreBattle( HeroBase & hero, bool isAttacking );
static void CastlePreBattle( Castle & castle );

Expand Down
105 changes: 105 additions & 0 deletions src/fheroes2/ai/ai_planner_hero.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,84 @@ namespace
return fogDiscoveryBaseValue;
}

double getSecondarySkillValue( const Heroes & hero, const Skill::Secondary & skill )
{
const int type = skill.Skill();
const int level = skill.Level();
if ( hero.GetLevelSkill( type ) >= level ) {
return 0;
}

switch ( type ) {
case Skill::Secondary::WISDOM:
// wouldn't check castles/spell availability since high wisdom drives building priority
return level == Skill::Level::BASIC ? 2500.0 : 1000.0;
case Skill::Secondary::LOGISTICS:
return 1500.0;
case Skill::Secondary::LEADERSHIP:
return hero.GetArmy().AllTroopsAreUndead() ? 100.0 : 1000.0;
case Skill::Secondary::NECROMANCY:
return hero.GetArmy().AllTroopsAreUndead() ? 1000.0 : 100.0;
case Skill::Secondary::LUCK: {
const Heroes::Role role = hero.getAIRole();
if ( role == Heroes::Role::COURIER || role == Heroes::Role::SCOUT ) {
return 100.0;
}
return 500.0;
}
case Skill::Secondary::BALLISTICS: {
const Heroes::Role role = hero.getAIRole();
if ( role == Heroes::Role::COURIER || role == Heroes::Role::SCOUT ) {
return 100.0;
}
return hero.GetArmy().isMeleeDominantArmy() ? 1250.0 : 250.0;
}
case Skill::Secondary::ARCHERY: {
const Heroes::Role role = hero.getAIRole();
if ( role == Heroes::Role::COURIER || role == Heroes::Role::SCOUT ) {
return 100.0;
}
return hero.GetArmy().isMeleeDominantArmy() ? 100.0 : 500.0;
}
case Skill::Secondary::ESTATES: {
const Heroes::Role role = hero.getAIRole();
if ( role == Heroes::Role::CHAMPION || role == Heroes::Role::FIGHTER ) {
return 0.0;
}
return 1000.0;
}
case Skill::Secondary::PATHFINDING: {
const double roughness = world.getLandRoughness();
return ( roughness > 1.25 ) ? 1000.0 : ( roughness > 1.1 ) ? 250.0 : 100.0;
ihhub marked this conversation as resolved.
Show resolved Hide resolved
}
case Skill::Secondary::NAVIGATION: {
const uint8_t waterPercentage = world.getWaterPercentage();
return ( waterPercentage > 60 ) ? 1000.0 : ( waterPercentage > 25 ) ? 100.0 : 0.0;
ihhub marked this conversation as resolved.
Show resolved Hide resolved
}
case Skill::Secondary::SCOUTING: {
const Heroes::Role role = hero.getAIRole();
if ( role == Heroes::Role::CHAMPION || role == Heroes::Role::FIGHTER ) {
return 0.0;
}
return hero.getAIRole() == Heroes::Role::SCOUT ? 1250.0 : 100.0;
}
case Skill::Secondary::MYSTICISM:
return hero.HaveSpellBook() ? 500.0 : 100.0;
case Skill::Secondary::EAGLE_EYE:
return hero.HaveSpellBook() ? 250.0 : 0.0;
case Skill::Secondary::DIPLOMACY:
// discourage AI picking it up, but if it's already there prioritise leveling to save gold
return level == Skill::Level::BASIC ? 100.0 : 1250.0;
case Skill::Secondary::UNKNOWN:
return 0;
default:
// If you make a new secondary skill don't forget to update this.
assert( 0 );
break;
}
return 0;
}

uint32_t getTimeoutBeforeFogDiscoveryIntensification( const Heroes & hero )
{
switch ( hero.getAIRole() ) {
Expand Down Expand Up @@ -2511,6 +2589,33 @@ void AI::Planner::updatePriorityTargets( Heroes & hero, int32_t tileIndex, const
}
}

Skill::Secondary AI::Planner::pickSecondarySkill( const Heroes & hero, const Skill::Secondary & left, const Skill::Secondary & right )
{
// heroes can get 1 or 0 skill choices depending on a level
if ( !right.isValid() ) {
// if both left and right are invalid returning either is fine
return left;
}

const double leftValue = getSecondarySkillValue( hero, left );
const double rightValue = getSecondarySkillValue( hero, right );

DEBUG_LOG( DBG_AI, DBG_TRACE,
hero.GetName() << " picking a skill: " << leftValue << " for " << left.String( left.first ) << " and " << rightValue << " for "
<< right.String( right.first ) )

if ( std::fabs( leftValue - rightValue ) < 0.001 ) {
// If skill value is lower than the threshold then it's undesireable. Avoid learning it.
if ( leftValue < 300.0 ) {
return left.Level() == Skill::Level::BASIC ? right : left;
}

return left.Level() == Skill::Level::BASIC ? left : right;
}

return leftValue > rightValue ? left : right;
}

void AI::Planner::HeroesBeginMovement( Heroes & hero )
{
assert( hero.isActive() );
Expand Down
5 changes: 4 additions & 1 deletion src/fheroes2/heroes/heroes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1828,7 +1828,7 @@ void Heroes::LevelUp( bool skipsecondary, bool autoselect )
DEBUG_LOG( DBG_GAME, DBG_INFO, "for " << GetName() << ", up " << Skill::Primary::String( primarySkill ) )

if ( !skipsecondary ) {
LevelUpSecondarySkill( seeds, primarySkill, ( autoselect || isControlAI() ) );
LevelUpSecondarySkill( seeds, primarySkill, autoselect );
}
}

Expand Down Expand Up @@ -1856,6 +1856,9 @@ void Heroes::LevelUpSecondarySkill( const HeroSeedsForLevelUp & seeds, int prima
selected = sec1.isValid() ? sec1 : sec2;
}
}
else if ( isControlAI() ) {
selected = AI::Planner::Get().pickSecondarySkill( *this, sec1, sec2 );
}
else {
AudioManager::PlaySound( M82::NWHEROLV );
const int result = Dialog::LevelUpSelectSkill( name, primary, sec1, sec2, *this );
Expand Down
12 changes: 12 additions & 0 deletions src/fheroes2/world/world.h
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ class World : protected fheroes2::Size
const MapRegion & getRegion( size_t id ) const;
size_t getRegionCount() const;

uint8_t getWaterPercentage() const
{
return _waterPercentage;
}

double getLandRoughness() const
{
return _landRoughness;
}

uint32_t getDistance( const Heroes & hero, int targetIndex );
std::list<Route::Step> getPath( const Heroes & hero, int targetIndex );
void resetPathfinder();
Expand Down Expand Up @@ -413,6 +423,8 @@ class World : protected fheroes2::Size
std::map<uint8_t, Maps::Indexes> _allTeleports; // All indexes of tiles that contain stone liths of a certain type (sprite index)
std::map<uint8_t, Maps::Indexes> _allWhirlpools; // All indexes of tiles that contain a certain part (sprite index) of the whirlpool

uint8_t _waterPercentage{ 0 };
double _landRoughness{ 1.0 };
std::vector<MapRegion> _regions;
PlayerWorldPathfinder _pathfinder;
};
Expand Down
22 changes: 21 additions & 1 deletion src/fheroes2/world/world_regions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/

#include "world_regions.h"

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <memory>
Expand All @@ -27,12 +30,12 @@
#include <vector>

#include "castle.h"
#include "ground.h"
#include "maps.h"
#include "maps_tiles.h"
#include "math_base.h"
#include "mp2.h"
#include "world.h"
#include "world_regions.h"

namespace
{
Expand Down Expand Up @@ -200,6 +203,10 @@ void World::ComputeStaticAnalysis()
obstacles[3].emplace_back( y, 0 ); // ground, rows
}

int obstacleCount = 0;
int waterCount = 0;
uint32_t terrainPenalty = 0;

// Find the terrain
for ( int y = 0; y < height; ++y ) {
const int rowIndex = y * width;
Expand All @@ -208,24 +215,37 @@ void World::ComputeStaticAnalysis()
const Maps::Tiles & tile = vec_tiles[index];
// If tile is blocked (mountain, trees, etc) then it's applied to both
if ( tile.GetPassable() == 0 ) {
++obstacleCount;
++obstacles[0][x].second;
++obstacles[1][y].second;
++obstacles[2][x].second;
++obstacles[3][y].second;
}
else if ( tile.isWater() ) {
++waterCount;
// if it's water then ground tiles consider it an obstacle
++obstacles[2][x].second;
++obstacles[3][y].second;
}
else {
terrainPenalty += Maps::Ground::GetPenalty( tile, 0 );
// else then ground is an obstacle for water navigation
++obstacles[0][x].second;
++obstacles[1][y].second;
}
}
}

const int passableTileCount = ( width * height ) - obstacleCount;
assert( passableTileCount > 0 );

_waterPercentage = static_cast<uint8_t>( waterCount * 100 / passableTileCount );

const int landTiles = passableTileCount - waterCount;
idshibanov marked this conversation as resolved.
Show resolved Hide resolved
assert( landTiles > 0 );

_landRoughness = static_cast<double>( terrainPenalty ) / ( landTiles * Maps::Ground::defaultGroundPenalty );

// sort the map rows and columns based on amount of obstacles
for ( int i = 0; i < 4; ++i )
std::sort( obstacles[i].begin(), obstacles[i].end(), []( const TileData & left, const TileData & right ) { return left.second < right.second; } );
Expand Down
Loading