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
6 changes: 6 additions & 0 deletions src/fheroes2/ai/ai.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ namespace Battle
class Actions;
}

namespace Skill
{
class Secondary;
}

namespace AI
{
enum class AIType : int
Expand Down Expand Up @@ -112,6 +117,7 @@ namespace AI
virtual void battleBegins() = 0;

virtual void tradingPostVisitEvent( Kingdom & kingdom ) = 0;
virtual Skill::Secondary pickSecondarySkill( const Heroes & hero, const Skill::Secondary & left, const Skill::Secondary & right ) = 0;

protected:
Base() = default;
Expand Down
2 changes: 2 additions & 0 deletions src/fheroes2/ai/normal/ai_normal.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "mp2.h"
#include "pairs.h"
#include "resource.h"
#include "skill.h"
#include "world_pathfinding.h"

class Castle;
Expand Down Expand Up @@ -309,6 +310,7 @@ namespace AI
void battleBegins() override;

void tradingPostVisitEvent( Kingdom & kingdom ) override;
Skill::Secondary pickSecondarySkill( const Heroes & hero, const Skill::Secondary & left, const Skill::Secondary & right ) 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 );
Expand Down
101 changes: 101 additions & 0 deletions src/fheroes2/ai/normal/ai_normal_hero.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,84 @@ namespace
return fogDiscoveryBaseValue;
}

double getSecondarySkillValue( const Heroes & hero, const Skill::Secondary & skill )
ihhub marked this conversation as resolved.
Show resolved Hide resolved
{
const int type = skill.Skill();
ihhub marked this conversation as resolved.
Show resolved Hide resolved
const int level = skill.Level();
if ( hero.GetLevelSkill( skill.first ) >= level ) {
idshibanov marked this conversation as resolved.
Show resolved Hide resolved
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;
ihhub marked this conversation as resolved.
Show resolved Hide resolved
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();
ihhub marked this conversation as resolved.
Show resolved Hide resolved
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;
}
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;
ihhub marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -2485,6 +2563,29 @@ namespace AI
}
}

Skill::Secondary Normal::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;
}
DEBUG_LOG( DBG_AI, DBG_TRACE,
hero.GetName() << " picking a skill: " << getSecondarySkillValue( hero, left ) << " for " << left.String( left.first ) << " and "
<< getSecondarySkillValue( hero, right ) << " for " << right.String( right.first ) )

const double leftValue = getSecondarySkillValue( hero, left );
idshibanov marked this conversation as resolved.
Show resolved Hide resolved
const double rightValue = getSecondarySkillValue( hero, right );
idshibanov marked this conversation as resolved.
Show resolved Hide resolved

if ( std::fabs( leftValue - rightValue ) < 0.001 ) {
if ( leftValue < 300.0 ) {
idshibanov marked this conversation as resolved.
Show resolved Hide resolved
return left.Level() == Skill::Level::BASIC ? right : left;
}
return left.Level() == Skill::Level::BASIC ? left : right;
}
return leftValue > rightValue ? left : right;
idshibanov marked this conversation as resolved.
Show resolved Hide resolved
}

void Normal::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 @@ -1833,7 +1833,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 );
if ( isControlAI() )
AI::Get().HeroesLevelUp( *this );
}
Expand Down Expand Up @@ -1862,6 +1862,9 @@ void Heroes::LevelUpSecondarySkill( const HeroSeedsForLevelUp & seeds, int prima
selected = sec1.isValid() ? sec1 : sec2;
}
}
else if ( isControlAI() ) {
selected = AI::Get().pickSecondarySkill( *this, sec1, sec2 );
}
else {
AudioManager::PlaySound( M82::NWHEROLV );
const int result = Dialog::LevelUpSelectSkill( name, primary, sec1, sec2, *this );
Expand Down
10 changes: 10 additions & 0 deletions src/fheroes2/world/world.h
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,14 @@ 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 @@ -410,6 +418,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;
idshibanov marked this conversation as resolved.
Show resolved Hide resolved
std::vector<MapRegion> _regions;
PlayerWorldPathfinder _pathfinder;
};
Expand Down
21 changes: 19 additions & 2 deletions src/fheroes2/world/world_regions.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/***************************************************************************
* fheroes2: https://github.com/ihhub/fheroes2 *
* Copyright (C) 2020 - 2023 *
* Copyright (C) 2020 - 2024 *
* *
* 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 *
Expand All @@ -18,6 +18,8 @@
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/

#include "world_regions.h"

#include <algorithm>
#include <cstddef>
#include <cstdint>
Expand All @@ -27,12 +29,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 +202,10 @@ void World::ComputeStaticAnalysis()
obstacles[3].emplace_back( y, 0 ); // ground, rows
}

int obstacleCount = 0;
int waterCount = 0;
double terrainPenalty = 0;
idshibanov marked this conversation as resolved.
Show resolved Hide resolved

// Find the terrain
for ( int y = 0; y < height; ++y ) {
const int rowIndex = y * width;
Expand All @@ -208,24 +214,35 @@ 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
landRoughness = ( landTiles > 0 ) ? terrainPenalty / ( landTiles * Maps::Ground::defaultGroundPenalty ) : 1.0;

// 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