Skip to content

Commit

Permalink
AI: implement the logic to transfer the slowest troops to the garriso…
Browse files Browse the repository at this point in the history
…n at the end of the turn to try to get a movement bonus on the next turn (ihhub#8212)
  • Loading branch information
oleg-derevenetz committed Jan 2, 2024
1 parent 23d3cd2 commit f487fd7
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 59 deletions.
13 changes: 9 additions & 4 deletions src/fheroes2/ai/ai.h
Original file line number Diff line number Diff line change
@@ -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) 2010 by Andrey Afletdinov <fheroes2@gmail.com> *
Expand Down Expand Up @@ -86,6 +86,8 @@ namespace AI
class Base
{
public:
virtual ~Base() = default;

virtual void KingdomTurn( Kingdom & kingdom ) = 0;
virtual void BattleTurn( Battle::Arena & arena, const Battle::Unit & unit, Battle::Actions & actions ) = 0;

Expand Down Expand Up @@ -119,8 +121,6 @@ namespace AI
// involved in the battle - because of the possibility of using instant or auto battle
virtual void battleBegins() = 0;

virtual ~Base() = default;

virtual void tradingPostVisitEvent( Kingdom & kingdom ) = 0;

protected:
Expand Down Expand Up @@ -165,7 +165,12 @@ namespace AI
// returns false.
bool BuildIfEnoughFunds( Castle & castle, const building_t building, const uint32_t fundsMultiplier );

void OptimizeTroopsOrder( Army & hero );
// Performs the pre-battle arrangement of the given army, see the implementation for details
void OptimizeTroopsOrder( Army & army );

// Transfers the slowest troops from the hero's army to the garrison to try to get a movement bonus on the next turn
void transferSlowestTroopsToGarrison( Heroes * hero, Castle * castle );

bool CanPurchaseHero( const Kingdom & kingdom );

// Calculates a marketplace transaction, after which the kingdom would be able to make a payment in the amount of
Expand Down
74 changes: 60 additions & 14 deletions src/fheroes2/ai/ai_common.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 Down Expand Up @@ -35,6 +35,7 @@
#include "color.h"
#include "difficulty.h"
#include "game.h"
#include "heroes.h"
#include "kingdom.h"
#include "logging.h"
#include "normal/ai_normal.h"
Expand Down Expand Up @@ -102,10 +103,8 @@ namespace AI
return;
}

// Optimize troops placement in case of a battle
army.MergeSameMonsterTroops();

// Validate and pick the troops
std::vector<Troop> archers;
std::vector<Troop> others;

Expand All @@ -121,21 +120,23 @@ namespace AI
}
}

// Sort troops by tactical priority. For melee:
// 1. Faster units first
// 2. Flyers first
// 3. Finally if unit type and speed is same, compare by strength
// Sort troops by tactical priority. For melee units, the order of comparison is as follows:
// 1. Comparison by speed (faster units first);
// 2. Comparison by type (flying units first);
// 3. Comparison by strength.
std::sort( others.begin(), others.end(), []( const Troop & left, const Troop & right ) {
if ( left.GetSpeed() == right.GetSpeed() ) {
if ( left.isFlying() == right.isFlying() ) {
return left.GetStrength() < right.GetStrength();
}
if ( left.GetSpeed() != right.GetSpeed() ) {
return left.GetSpeed() < right.GetSpeed();
}

if ( left.isFlying() != right.isFlying() ) {
return right.isFlying();
}
return left.GetSpeed() < right.GetSpeed();

return left.GetStrength() < right.GetStrength();
} );

// Archers sorted purely by strength.
// Archers are sorted solely by strength
std::sort( archers.begin(), archers.end(), []( const Troop & left, const Troop & right ) { return left.GetStrength() < right.GetStrength(); } );

std::vector<size_t> slotOrder = { 2, 1, 3, 0, 4 };
Expand All @@ -157,15 +158,17 @@ namespace AI
break;
}

// Re-arrange troops in army
army.Clean();

for ( const size_t slot : slotOrder ) {
if ( !archers.empty() ) {
army.GetTroop( slot )->Set( archers.back() );

archers.pop_back();
}
else if ( !others.empty() ) {
army.GetTroop( slot )->Set( others.back() );

others.pop_back();
}
else {
Expand All @@ -179,6 +182,49 @@ namespace AI
}
}

void transferSlowestTroopsToGarrison( Heroes * hero, Castle * castle )
{
assert( hero != nullptr && castle != nullptr );

Army & army = hero->GetArmy();
Army & garrison = castle->GetArmy();

// Make efforts to get free slots in the garrison to move troops there
garrison.MergeSameMonsterTroops();

std::vector<Troop *> armyTroops;
armyTroops.reserve( army.Size() );

for ( size_t i = 0; i < army.Size(); ++i ) {
Troop * troop = army.GetTroop( i );
assert( troop != nullptr );

if ( troop->isEmpty() ) {
continue;
}

armyTroops.push_back( troop );
}

assert( !armyTroops.empty() );

// Move the slowest units first
std::sort( armyTroops.begin(), armyTroops.end(), Army::SlowestTroop );

// At least one of the fastest units should remain in the hero's army
armyTroops.pop_back();

for ( Troop * troop : armyTroops ) {
if ( !garrison.JoinTroop( *troop ) ) {
break;
}

troop->Reset();
}

assert( army.isValid() );
}

bool CanPurchaseHero( const Kingdom & kingdom )
{
if ( kingdom.GetCountCastle() == 0 ) {
Expand Down
42 changes: 22 additions & 20 deletions src/fheroes2/ai/normal/ai_normal.h
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 Down Expand Up @@ -271,18 +271,21 @@ namespace AI
// Implements the logic of transparent casting of the Summon Boat spell during the hero's movement
void HeroesActionNewPosition( Heroes & hero ) override;

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<AICastle> getSortedCastleList( const VecCastles & castles, const std::set<int> & castlesInDanger );

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;
int getPriorityTarget( const HeroToMove & heroInfo, double & maxPriority );
void resetPathfinder() override;
bool isValidHeroObject( const Heroes & hero, const int32_t index, const bool underHero ) override;

void battleBegins() override;

void tradingPostVisitEvent( Kingdom & kingdom ) 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
Expand All @@ -300,28 +303,14 @@ namespace AI
return iter->second.type == PriorityTaskType::ATTACK || iter->second.type == PriorityTaskType::DEFEND;
}

void tradingPostVisitEvent( Kingdom & kingdom ) override;

private:
// following data won't be saved/serialized
double _combinedHeroStrength = 0;
std::vector<IndexObject> _mapActionObjects;
std::map<int32_t, PriorityTask> _priorityTargets;
std::map<int32_t, EnemyArmy> _enemyArmies;
std::vector<RegionStats> _regions;
std::array<BudgetEntry, 7> _budget = { Resource::WOOD, Resource::MERCURY, Resource::ORE, Resource::SULFUR, Resource::CRYSTAL, Resource::GEMS, Resource::GOLD };
AIWorldPathfinder _pathfinder;
BattlePlanner _battlePlanner;

// Monster strength is constant over the same turn for AI but its calculation is a heavy operation.
// In order to avoid extra computations during AI turn it is important to keep cache of monster strength but update it when an action on a monster is taken.
std::map<int32_t, double> _neutralMonsterStrengthCache;

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 );

int getPriorityTarget( const HeroToMove & heroInfo, double & maxPriority );

double getGeneralObjectValue( const Heroes & hero, const int index, const double valueToIgnore, const uint32_t distanceToObject ) const;
double getFighterObjectValue( const Heroes & hero, const int index, const double valueToIgnore, const uint32_t distanceToObject ) const;
double getCourierObjectValue( const Heroes & hero, const int index, const double valueToIgnore, const uint32_t distanceToObject ) const;
Expand Down Expand Up @@ -354,6 +343,19 @@ namespace AI
void removePriorityAttackTarget( const int32_t tileIndex );

void updatePriorityAttackTarget( const Kingdom & kingdom, const Maps::Tiles & tile );

// The following member variables should not be saved or serialized
std::vector<IndexObject> _mapActionObjects;
std::map<int32_t, PriorityTask> _priorityTargets;
std::map<int32_t, EnemyArmy> _enemyArmies;
std::vector<RegionStats> _regions;
std::array<BudgetEntry, 7> _budget = { Resource::WOOD, Resource::MERCURY, Resource::ORE, Resource::SULFUR, Resource::CRYSTAL, Resource::GEMS, Resource::GOLD };
AIWorldPathfinder _pathfinder;
BattlePlanner _battlePlanner;

// Monster strength is constant over the same turn for AI but its calculation is a heavy operation.
// In order to avoid extra computations during AI turn it is important to keep cache of monster strength but update it when an action on a monster is taken.
std::map<int32_t, double> _neutralMonsterStrengthCache;
};
}

Expand Down
20 changes: 19 additions & 1 deletion src/fheroes2/ai/normal/ai_normal_castle.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 Down Expand Up @@ -236,6 +236,24 @@ namespace AI
return Build( castle, supportingDefensiveStructures, 10 );
}

void Normal::CastlePreBattle( Castle & castle )
{
Heroes * hero = world.GetHero( castle );
if ( hero == nullptr ) {
return;
}

Army & army = hero->GetArmy();

if ( !army.ArrangeForCastleDefense( castle.GetArmy() ) ) {
return;
}

// Optimization cannot be performed if we have not received any reinforcements from the garrison, otherwise the actual placement of units during the battle will
// differ from that observed by the enemy player before the start of the battle
OptimizeTroopsOrder( army );
}

void Normal::updateKingdomBudget( const Kingdom & kingdom )
{
// clean up first
Expand Down
24 changes: 17 additions & 7 deletions src/fheroes2/ai/normal/ai_normal_kingdom.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 Down Expand Up @@ -716,7 +716,7 @@ namespace AI
return;
}

// reset indicator
// Reset the turn progress indicator
Interface::StatusWindow & status = Interface::AdventureMap::Get().getStatusWindow();
status.DrawAITurnProgress( 0 );

Expand All @@ -740,10 +740,9 @@ namespace AI
hero->ResetModes( Heroes::SLEEPER );
hero->setDimensionDoorUsage( 0 );

const double strength = hero->GetArmy().GetStrength();
_combinedHeroStrength += strength;
if ( !hero->Modes( Heroes::PATROL ) )
if ( !hero->Modes( Heroes::PATROL ) ) {
++availableHeroCount;
}

if ( hero->HaveSpell( Spell::VIEWALL ) && ( !bestHeroToViewAll || hero->HasSecondarySkill( Skill::Secondary::MYSTICISM ) ) ) {
bestHeroToViewAll = hero;
Expand All @@ -754,13 +753,14 @@ namespace AI
underViewSpell = true;
}

const int mapSize = world.w() * world.h();
_priorityTargets.clear();
_enemyArmies.clear();
_mapActionObjects.clear();
_regions.clear();
_regions.resize( world.getRegionCount() );

const int mapSize = world.w() * world.h();

for ( int idx = 0; idx < mapSize; ++idx ) {
const Maps::Tiles & tile = world.GetTiles( idx );
MP2::MapObjectType objectType = tile.GetObject();
Expand Down Expand Up @@ -899,7 +899,7 @@ namespace AI

status.DrawAITurnProgress( 9 );

// sync up castle list (if conquered new ones during the turn)
// Sync the list of castles (if new ones were captured during the turn)
if ( castles.size() != sortedCastleList.size() ) {
evaluateRegionSafety();
sortedCastleList = getSortedCastleList( castles, castlesInDanger );
Expand All @@ -912,6 +912,16 @@ namespace AI
}
}

// For heroes in castles or towns, transfer their slowest troops to the garrison at the end of the turn to try to get a movement bonus on the next turn
for ( Heroes * hero : heroes ) {
Castle * castle = hero->inCastleMutable();
if ( castle == nullptr ) {
continue;
}

transferSlowestTroopsToGarrison( hero, castle );
}

status.DrawAITurnProgress( 10 );
}

Expand Down
Loading

0 comments on commit f487fd7

Please sign in to comment.