Skip to content

Commit

Permalink
AI should determine best skill when leveling up heroes (#8591)
Browse files Browse the repository at this point in the history
  • Loading branch information
idshibanov authored Jul 29, 2024
1 parent f24923a commit ce6aa75
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 1 deletion.
7 changes: 7 additions & 0 deletions src/fheroes2/ai/ai_planner.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ namespace Maps
class Tiles;
}

namespace Skill
{
class Secondary;
}

namespace AI
{
// Although AI heroes are capable to find their own tasks strategic AI should be able to focus them on most critical tasks
Expand Down Expand Up @@ -180,6 +185,8 @@ namespace AI
static void HeroesPreBattle( HeroBase & hero, bool isAttacking );
static void CastlePreBattle( Castle & castle );

static Skill::Secondary pickSecondarySkill( const Heroes & hero, const Skill::Secondary & left, const Skill::Secondary & right );

private:
Planner() = default;

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 @@ -877,6 +877,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;
}
case Skill::Secondary::NAVIGATION: {
const uint8_t waterPercentage = world.getWaterPercentage();
return ( waterPercentage > 60 ) ? 1000.0 : ( waterPercentage > 25 ) ? 100.0 : 0.0;
}
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 @@ -2508,6 +2586,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 @@ -1827,7 +1827,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 @@ -1855,6 +1855,9 @@ void Heroes::LevelUpSecondarySkill( const HeroSeedsForLevelUp & seeds, int prima
selected = sec1.isValid() ? sec1 : sec2;
}
}
else if ( isControlAI() ) {
selected = AI::Planner::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 @@ -353,6 +353,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 @@ -417,6 +427,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
19 changes: 19 additions & 0 deletions src/fheroes2/world/world_regions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
#include "world_regions.h"

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <set>
#include <utility>
#include <vector>

#include "castle.h"
#include "ground.h"
#include "maps.h"
#include "maps_tiles.h"
#include "math_base.h"
Expand Down Expand Up @@ -200,6 +202,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 +214,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;
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

0 comments on commit ce6aa75

Please sign in to comment.