mirror of
https://github.com/ihhub/fheroes2.git
synced 2026-10-12 02:16:11 +00:00
Improve path calculations by AIWorldPathfinder in planning mode (#7739)
This commit is contained in:
@@ -244,7 +244,6 @@
|
||||
<ClInclude Include="src\engine\localevent.h" />
|
||||
<ClInclude Include="src\engine\math_base.h" />
|
||||
<ClInclude Include="src\engine\pal.h" />
|
||||
<ClInclude Include="src\engine\pathfinding.h" />
|
||||
<ClInclude Include="src\engine\rand.h" />
|
||||
<ClInclude Include="src\engine\screen.h" />
|
||||
<ClInclude Include="src\engine\serialize.h" />
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/***************************************************************************
|
||||
* fheroes2: https://github.com/ihhub/fheroes2 *
|
||||
* Copyright (C) 2020 - 2023 *
|
||||
* *
|
||||
* 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 *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// Base representation of the dataset that mirrors the 2D map being traversed
|
||||
template <class T>
|
||||
struct PathfindingNode
|
||||
{
|
||||
int _from = -1;
|
||||
uint32_t _cost = 0;
|
||||
T _objectID{};
|
||||
|
||||
PathfindingNode() = default;
|
||||
PathfindingNode( int node, uint32_t cost, T object )
|
||||
: _from( node )
|
||||
, _cost( cost )
|
||||
, _objectID( object )
|
||||
{}
|
||||
virtual ~PathfindingNode() = default;
|
||||
// Sets node values back to the defaults; used before processing new path
|
||||
virtual void resetNode()
|
||||
{
|
||||
_from = -1;
|
||||
_cost = 0;
|
||||
_objectID = T();
|
||||
}
|
||||
};
|
||||
|
||||
// Template class has to be either PathfindingNode or its derivative
|
||||
template <class T>
|
||||
class Pathfinder
|
||||
{
|
||||
public:
|
||||
virtual ~Pathfinder() = default;
|
||||
|
||||
virtual void reset() = 0;
|
||||
|
||||
virtual uint32_t getDistance( int targetIndex ) const
|
||||
{
|
||||
assert( targetIndex >= 0 && static_cast<size_t>( targetIndex ) < _cache.size() );
|
||||
|
||||
return _cache[targetIndex]._cost;
|
||||
}
|
||||
|
||||
virtual const T & getNode( int targetIndex ) const
|
||||
{
|
||||
assert( targetIndex >= 0 && static_cast<size_t>( targetIndex ) < _cache.size() );
|
||||
|
||||
return _cache[targetIndex];
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<T> _cache;
|
||||
int _pathStart = -1;
|
||||
};
|
||||
@@ -133,7 +133,7 @@ namespace
|
||||
return dist;
|
||||
}
|
||||
|
||||
const uint32_t dimensionDoorDist = AIWorldPathfinder::calculatePathPenalty( dimensionDoorSteps );
|
||||
const uint32_t dimensionDoorDist = Route::calculatePathPenalty( dimensionDoorSteps );
|
||||
assert( dimensionDoorDist > 0 );
|
||||
|
||||
if ( dist == 0 || dimensionDoorDist < dist / 2 ) {
|
||||
@@ -2038,7 +2038,7 @@ namespace AI
|
||||
uint32_t dist = _pathfinder.getDistance( node.first );
|
||||
|
||||
bool useDimensionDoor = false;
|
||||
const uint32_t dimensionDoorDist = AIWorldPathfinder::calculatePathPenalty( _pathfinder.getDimensionDoorPath( hero, node.first ) );
|
||||
const uint32_t dimensionDoorDist = Route::calculatePathPenalty( _pathfinder.getDimensionDoorPath( hero, node.first ) );
|
||||
if ( dimensionDoorDist > 0 && ( dist == 0 || dimensionDoorDist < dist / 2 ) ) {
|
||||
dist = dimensionDoorDist;
|
||||
useDimensionDoor = true;
|
||||
@@ -2073,7 +2073,7 @@ namespace AI
|
||||
|
||||
// TODO: add logic to check fog discovery based on Dimension Door distance, not the nearest tile.
|
||||
bool useDimensionDoor = false;
|
||||
const uint32_t dimensionDoorDist = AIWorldPathfinder::calculatePathPenalty( _pathfinder.getDimensionDoorPath( hero, fogDiscoveryTarget ) );
|
||||
const uint32_t dimensionDoorDist = Route::calculatePathPenalty( _pathfinder.getDimensionDoorPath( hero, fogDiscoveryTarget ) );
|
||||
if ( dimensionDoorDist > 0 && ( distanceToFogDiscovery == 0 || dimensionDoorDist < distanceToFogDiscovery / 2 ) ) {
|
||||
distanceToFogDiscovery = dimensionDoorDist;
|
||||
useDimensionDoor = true;
|
||||
@@ -2453,7 +2453,7 @@ namespace AI
|
||||
|
||||
// check if we want to use Dimension Door spell or move regularly
|
||||
std::list<Route::Step> dimensionPath = _pathfinder.getDimensionDoorPath( *bestHero, bestTargetIndex );
|
||||
uint32_t dimensionDoorDistance = AIWorldPathfinder::calculatePathPenalty( dimensionPath );
|
||||
uint32_t dimensionDoorDistance = Route::calculatePathPenalty( dimensionPath );
|
||||
uint32_t moveDistance = _pathfinder.getDistance( bestTargetIndex );
|
||||
if ( dimensionDoorDistance && ( !moveDistance || dimensionDoorDistance < moveDistance / 2 ) ) {
|
||||
while ( ( !moveDistance || dimensionDoorDistance < moveDistance / 2 ) && !dimensionPath.empty() && bestHero->MayStillMove( false, false )
|
||||
|
||||
@@ -50,12 +50,12 @@ namespace Battle
|
||||
|
||||
struct BattleNode final
|
||||
{
|
||||
BattleNodeIndex _from = { -1, -1 };
|
||||
BattleNodeIndex _from{ -1, -1 };
|
||||
// The cost of moving to this node. May differ from _distance due to penalties (e.g. moat penalty).
|
||||
uint32_t _cost = 0;
|
||||
uint32_t _cost{ 0 };
|
||||
// The distance to this node, measured in the number of movements that need to be performed to get here.
|
||||
// The reversal of a wide unit is not considered as a movement.
|
||||
uint32_t _distance = 0;
|
||||
uint32_t _distance{ 0 };
|
||||
|
||||
BattleNode() = default;
|
||||
BattleNode( BattleNodeIndex node, const uint32_t cost, const uint32_t distance )
|
||||
@@ -101,12 +101,12 @@ namespace Battle
|
||||
std::unordered_map<BattleNodeIndex, BattleNode, BattleNodeIndexHash> _cache;
|
||||
|
||||
// Parameters of the unit for which the current cache is created
|
||||
BattleNodeIndex _pathStart = { -1, -1 };
|
||||
uint32_t _speed = 0;
|
||||
bool _isWide = false;
|
||||
bool _isFlying = false;
|
||||
BattleNodeIndex _pathStart{ -1, -1 };
|
||||
uint32_t _speed{ 0 };
|
||||
bool _isWide{ false };
|
||||
bool _isFlying{ false };
|
||||
// The unit's color (or rather, the unit's army color) affects the ability to pass the castle bridge
|
||||
int _color = 0;
|
||||
int _color{ 0 };
|
||||
// Board cells passability status at the time of current cache creation
|
||||
std::bitset<ARENASIZE> _boardStatus;
|
||||
};
|
||||
|
||||
@@ -253,8 +253,9 @@ Heroes::Heroes( int heroid, int rc )
|
||||
break;
|
||||
}
|
||||
|
||||
if ( !magic_point )
|
||||
if ( !magic_point ) {
|
||||
SetSpellPoints( GetMaxSpellPoints() );
|
||||
}
|
||||
move_point = GetMaxMovePoints();
|
||||
}
|
||||
|
||||
@@ -702,67 +703,73 @@ uint32_t Heroes::GetMaxSpellPoints() const
|
||||
|
||||
uint32_t Heroes::GetMaxMovePoints() const
|
||||
{
|
||||
uint32_t point = 0;
|
||||
return GetMaxMovePoints( isShipMaster() );
|
||||
}
|
||||
|
||||
// start point
|
||||
if ( isShipMaster() ) {
|
||||
point = 1500;
|
||||
uint32_t Heroes::GetMaxMovePoints( const bool onWater ) const
|
||||
{
|
||||
uint32_t result = 0;
|
||||
|
||||
// skill navigation
|
||||
point = UpdateMovementPoints( point, Skill::Secondary::NAVIGATION );
|
||||
if ( onWater ) {
|
||||
// Initial mobility on water does not depend on the composition of the army
|
||||
result = 1500;
|
||||
|
||||
// artifact bonus
|
||||
point += GetBagArtifacts().getTotalArtifactEffectValue( fheroes2::ArtifactBonusType::SEA_MOBILITY );
|
||||
// Influence of Navigation skill
|
||||
result = UpdateMovementPoints( result, Skill::Secondary::NAVIGATION );
|
||||
|
||||
// visited object
|
||||
point += 500 * world.CountCapturedObject( MP2::OBJ_LIGHTHOUSE, GetColor() );
|
||||
// Artifact bonuses
|
||||
result += GetBagArtifacts().getTotalArtifactEffectValue( fheroes2::ArtifactBonusType::SEA_MOBILITY );
|
||||
|
||||
// Bonuses from captured lighthouses
|
||||
result += 500 * world.CountCapturedObject( MP2::OBJ_LIGHTHOUSE, GetColor() );
|
||||
}
|
||||
else {
|
||||
// Initial mobility on land depends on the speed of the slowest army unit
|
||||
const Troop * troop = army.GetSlowestTroop();
|
||||
|
||||
if ( troop )
|
||||
if ( troop ) {
|
||||
switch ( troop->GetSpeed() ) {
|
||||
default:
|
||||
break;
|
||||
case Speed::CRAWLING:
|
||||
case Speed::VERYSLOW:
|
||||
point = 1000;
|
||||
result = 1000;
|
||||
break;
|
||||
case Speed::SLOW:
|
||||
point = 1100;
|
||||
result = 1100;
|
||||
break;
|
||||
case Speed::AVERAGE:
|
||||
point = 1200;
|
||||
result = 1200;
|
||||
break;
|
||||
case Speed::FAST:
|
||||
point = 1300;
|
||||
result = 1300;
|
||||
break;
|
||||
case Speed::VERYFAST:
|
||||
point = 1400;
|
||||
result = 1400;
|
||||
break;
|
||||
case Speed::ULTRAFAST:
|
||||
case Speed::BLAZING:
|
||||
case Speed::INSTANT:
|
||||
point = 1500;
|
||||
result = 1500;
|
||||
break;
|
||||
default:
|
||||
assert( 0 );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// skill logistics
|
||||
point = UpdateMovementPoints( point, Skill::Secondary::LOGISTICS );
|
||||
// Influence of Logistics skill
|
||||
result = UpdateMovementPoints( result, Skill::Secondary::LOGISTICS );
|
||||
|
||||
// artifact bonus
|
||||
point += GetBagArtifacts().getTotalArtifactEffectValue( fheroes2::ArtifactBonusType::LAND_MOBILITY );
|
||||
// Artifact bonuses
|
||||
result += GetBagArtifacts().getTotalArtifactEffectValue( fheroes2::ArtifactBonusType::LAND_MOBILITY );
|
||||
|
||||
// visited object
|
||||
if ( isObjectTypeVisited( MP2::OBJ_STABLES ) )
|
||||
point += GameStatic::getMovementPointBonus( MP2::OBJ_STABLES );
|
||||
// Bonuses from visited objects
|
||||
if ( isObjectTypeVisited( MP2::OBJ_STABLES ) ) {
|
||||
result += GameStatic::getMovementPointBonus( MP2::OBJ_STABLES );
|
||||
}
|
||||
}
|
||||
|
||||
// AI-controlled heroes receive additional movement bonus depending on the game difficulty
|
||||
if ( isControlAI() ) {
|
||||
point += Difficulty::GetHeroMovementBonus( Game::getDifficulty() );
|
||||
result += Difficulty::GetHeroMovementBonus( Game::getDifficulty() );
|
||||
}
|
||||
|
||||
return point;
|
||||
return result;
|
||||
}
|
||||
|
||||
int Heroes::GetMorale() const
|
||||
|
||||
@@ -353,7 +353,11 @@ public:
|
||||
}
|
||||
|
||||
uint32_t GetMaxSpellPoints() const override;
|
||||
|
||||
// Returns the maximum number of hero movement points, depending on the surface type on which the hero is currently located
|
||||
uint32_t GetMaxMovePoints() const;
|
||||
// Returns the maximum number of hero movement points, depending on the specified surface type (water or land)
|
||||
uint32_t GetMaxMovePoints( const bool onWater ) const;
|
||||
|
||||
uint32_t GetMovePoints() const
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <numeric>
|
||||
|
||||
#include "game_io.h"
|
||||
#include "heroes.h"
|
||||
@@ -407,3 +408,8 @@ StreamBase & Route::operator>>( StreamBase & msg, Path & path )
|
||||
|
||||
return msg >> path._hide >> base;
|
||||
}
|
||||
|
||||
uint32_t Route::calculatePathPenalty( const std::list<Step> & path )
|
||||
{
|
||||
return std::accumulate( path.begin(), path.end(), static_cast<uint32_t>( 0 ), []( const uint32_t total, const Step & step ) { return total + step.GetPenalty(); } );
|
||||
}
|
||||
|
||||
@@ -201,6 +201,8 @@ namespace Route
|
||||
StreamBase & operator<<( StreamBase &, const Path & );
|
||||
StreamBase & operator>>( StreamBase &, Step & );
|
||||
StreamBase & operator>>( StreamBase &, Path & );
|
||||
|
||||
uint32_t calculatePathPenalty( const std::list<Step> & path );
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -339,31 +339,32 @@ namespace
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void WorldPathfinder::checkWorldSize()
|
||||
{
|
||||
const size_t worldSize = world.getSize();
|
||||
|
||||
if ( _cache.size() != worldSize ) {
|
||||
_cache.clear();
|
||||
_cache.resize( worldSize );
|
||||
|
||||
const Directions & directions = Direction::All();
|
||||
_mapOffset.resize( directions.size() );
|
||||
for ( size_t i = 0; i < directions.size(); ++i ) {
|
||||
_mapOffset[i] = Maps::GetDirectionIndex( 0, directions[i] );
|
||||
uint32_t subtractMovePoints( const uint32_t movePoints, const uint32_t subtractedMovePoints, const uint32_t maxMovePoints )
|
||||
{
|
||||
// We do not perform pathfinding for a real hero on the map, this is no-op
|
||||
if ( maxMovePoints == 0 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// This movement takes place at the beginning of a new turn: start with max movement points,
|
||||
// don't carry leftovers from the previous turn
|
||||
if ( movePoints < subtractedMovePoints ) {
|
||||
assert( maxMovePoints >= subtractedMovePoints );
|
||||
|
||||
return maxMovePoints - subtractedMovePoints;
|
||||
}
|
||||
|
||||
// This movement takes place on the same turn
|
||||
return movePoints - subtractedMovePoints;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t WorldPathfinder::calculatePathPenalty( const std::list<Route::Step> & path )
|
||||
uint32_t WorldPathfinder::getDistance( int targetIndex ) const
|
||||
{
|
||||
uint32_t dist = 0;
|
||||
for ( const Route::Step & step : path ) {
|
||||
dist += step.GetPenalty();
|
||||
}
|
||||
return dist;
|
||||
assert( targetIndex >= 0 && static_cast<size_t>( targetIndex ) < _cache.size() );
|
||||
|
||||
return _cache[targetIndex]._cost;
|
||||
}
|
||||
|
||||
uint32_t WorldPathfinder::getMovementPenalty( const int from, const int to, const int direction ) const
|
||||
@@ -382,7 +383,7 @@ uint32_t WorldPathfinder::getMovementPenalty( const int from, const int to, cons
|
||||
// logic: if this move is the last one on the current turn, then we can move to any adjacent
|
||||
// tile (both in straight and diagonal direction) as long as we have enough movement points
|
||||
// to move over our current tile in the straight direction
|
||||
if ( _maxMovePoints > 0 ) {
|
||||
if ( getMaxMovePoints( fromTile.isWater() ) > 0 ) {
|
||||
const WorldNode & node = _cache[from];
|
||||
|
||||
// No dead ends allowed
|
||||
@@ -403,31 +404,39 @@ uint32_t WorldPathfinder::getMovementPenalty( const int from, const int to, cons
|
||||
return penalty;
|
||||
}
|
||||
|
||||
uint32_t WorldPathfinder::subtractMovePoints( const uint32_t movePoints, const uint32_t subtractedMovePoints ) const
|
||||
void WorldPathfinder::reset()
|
||||
{
|
||||
// We do not perform pathfinding for a real hero on the map, this is no-op
|
||||
if ( _maxMovePoints == 0 ) {
|
||||
return 0;
|
||||
// The following optimization will only work correctly for square maps
|
||||
assert( world.w() == world.h() );
|
||||
|
||||
const size_t worldSize = world.getSize();
|
||||
|
||||
if ( _cache.size() != worldSize ) {
|
||||
_cache.clear();
|
||||
_cache.resize( worldSize );
|
||||
|
||||
const Directions & directions = Direction::All();
|
||||
_mapOffset.resize( directions.size() );
|
||||
|
||||
for ( size_t i = 0; i < directions.size(); ++i ) {
|
||||
_mapOffset[i] = Maps::GetDirectionIndex( 0, directions[i] );
|
||||
}
|
||||
}
|
||||
|
||||
// This movement takes place at the beginning of a new turn: start with max movement points,
|
||||
// don't carry leftovers from the previous turn
|
||||
if ( movePoints < subtractedMovePoints ) {
|
||||
assert( _maxMovePoints >= subtractedMovePoints );
|
||||
|
||||
return _maxMovePoints - subtractedMovePoints;
|
||||
}
|
||||
|
||||
// This movement takes place on the same turn
|
||||
return movePoints - subtractedMovePoints;
|
||||
_pathStart = -1;
|
||||
_color = Color::NONE;
|
||||
_remainingMovePoints = 0;
|
||||
_pathfindingSkill = Skill::Level::EXPERT;
|
||||
}
|
||||
|
||||
void WorldPathfinder::processWorldMap()
|
||||
{
|
||||
// reset cache back to default value
|
||||
for ( size_t idx = 0; idx < _cache.size(); ++idx ) {
|
||||
_cache[idx].resetNode();
|
||||
assert( _cache.size() == world.getSize() && Maps::isValidAbsIndex( _pathStart ) );
|
||||
|
||||
for ( WorldNode & node : _cache ) {
|
||||
node.reset();
|
||||
}
|
||||
|
||||
_cache[_pathStart] = WorldNode( -1, 0, MP2::OBJ_NONE, _remainingMovePoints );
|
||||
|
||||
std::vector<int> nodesToExplore;
|
||||
@@ -442,6 +451,7 @@ void WorldPathfinder::checkAdjacentNodes( std::vector<int> & nodesToExplore, int
|
||||
{
|
||||
const Directions & directions = Direction::All();
|
||||
const WorldNode & currentNode = _cache[currentNodeIdx];
|
||||
const uint32_t maxMovePoints = getMaxMovePoints( world.GetTiles( currentNodeIdx ).isWater() );
|
||||
|
||||
for ( size_t i = 0; i < directions.size(); ++i ) {
|
||||
if ( !Maps::isValidDirection( currentNodeIdx, directions[i] ) || !isMovementAllowed( currentNodeIdx, directions[i] ) ) {
|
||||
@@ -464,7 +474,7 @@ void WorldPathfinder::checkAdjacentNodes( std::vector<int> & nodesToExplore, int
|
||||
newNode._from = currentNodeIdx;
|
||||
newNode._cost = movementCost;
|
||||
newNode._objectID = newTile.GetObject();
|
||||
newNode._remainingMovePoints = subtractMovePoints( currentNode._remainingMovePoints, movementPenalty );
|
||||
newNode._remainingMovePoints = subtractMovePoints( currentNode._remainingMovePoints, movementPenalty, maxMovePoints );
|
||||
|
||||
nodesToExplore.push_back( newIndex );
|
||||
}
|
||||
@@ -478,21 +488,16 @@ bool WorldPathfinder::isMovementAllowed( const int from, const int direction ) c
|
||||
|
||||
void PlayerWorldPathfinder::reset()
|
||||
{
|
||||
WorldPathfinder::checkWorldSize();
|
||||
WorldPathfinder::reset();
|
||||
|
||||
_pathStart = -1;
|
||||
|
||||
_color = Color::NONE;
|
||||
_remainingMovePoints = 0;
|
||||
_maxMovePoints = 0;
|
||||
_pathfindingSkill = Skill::Level::EXPERT;
|
||||
}
|
||||
|
||||
void PlayerWorldPathfinder::reEvaluateIfNeeded( const Heroes & hero )
|
||||
{
|
||||
auto currentSettings = std::tie( _pathStart, _color, _remainingMovePoints, _maxMovePoints, _pathfindingSkill );
|
||||
const auto newSettings = std::make_tuple( hero.GetIndex(), hero.GetColor(), hero.GetMovePoints(), hero.GetMaxMovePoints(),
|
||||
static_cast<uint8_t>( hero.GetLevelSkill( Skill::Secondary::PATHFINDING ) ) );
|
||||
auto currentSettings = std::tie( _pathStart, _color, _remainingMovePoints, _pathfindingSkill, _maxMovePoints );
|
||||
const auto newSettings = std::make_tuple( hero.GetIndex(), hero.GetColor(), hero.GetMovePoints(),
|
||||
static_cast<uint8_t>( hero.GetLevelSkill( Skill::Secondary::PATHFINDING ) ), hero.GetMaxMovePoints() );
|
||||
|
||||
if ( currentSettings != newSettings ) {
|
||||
currentSettings = newSettings;
|
||||
@@ -503,7 +508,7 @@ void PlayerWorldPathfinder::reEvaluateIfNeeded( const Heroes & hero )
|
||||
|
||||
std::list<Route::Step> PlayerWorldPathfinder::buildPath( const int targetIndex ) const
|
||||
{
|
||||
assert( _pathStart != -1 && targetIndex != -1 );
|
||||
assert( _cache.size() == world.getSize() && Maps::isValidAbsIndex( _pathStart ) && Maps::isValidAbsIndex( targetIndex ) );
|
||||
|
||||
std::list<Route::Step> path;
|
||||
|
||||
@@ -552,6 +557,8 @@ void PlayerWorldPathfinder::processCurrentNode( std::vector<int> & nodesToExplor
|
||||
|
||||
// If the current tile is protected by monsters, and this tile is not the starting tile, then the hero can only move towards one of the neighboring monsters
|
||||
if ( !isFirstNode && !monsters.empty() ) {
|
||||
const uint32_t maxMovePoints = getMaxMovePoints( fromWater );
|
||||
|
||||
for ( int monsterIndex : monsters ) {
|
||||
const int direction = Maps::GetDirection( currentNodeIdx, monsterIndex );
|
||||
|
||||
@@ -570,7 +577,7 @@ void PlayerWorldPathfinder::processCurrentNode( std::vector<int> & nodesToExplor
|
||||
monsterNode._from = currentNodeIdx;
|
||||
monsterNode._cost = movementCost;
|
||||
monsterNode._objectID = monsterTile.GetObject();
|
||||
monsterNode._remainingMovePoints = subtractMovePoints( currentNode._remainingMovePoints, movementPenalty );
|
||||
monsterNode._remainingMovePoints = subtractMovePoints( currentNode._remainingMovePoints, movementPenalty, maxMovePoints );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -579,17 +586,17 @@ void PlayerWorldPathfinder::processCurrentNode( std::vector<int> & nodesToExplor
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t PlayerWorldPathfinder::getMaxMovePoints( const bool /* onWater */ ) const
|
||||
{
|
||||
return _maxMovePoints;
|
||||
}
|
||||
|
||||
void AIWorldPathfinder::reset()
|
||||
{
|
||||
WorldPathfinder::checkWorldSize();
|
||||
|
||||
_pathStart = -1;
|
||||
|
||||
_color = Color::NONE;
|
||||
_remainingMovePoints = 0;
|
||||
_maxMovePoints = 0;
|
||||
_pathfindingSkill = Skill::Level::EXPERT;
|
||||
WorldPathfinder::reset();
|
||||
|
||||
_maxMovePointsOnLand = 0;
|
||||
_maxMovePointsOnWater = 0;
|
||||
_armyStrength = -1;
|
||||
_isArtifactsBagFull = false;
|
||||
_isSummonBoatSpellAvailable = false;
|
||||
@@ -655,11 +662,12 @@ void AIWorldPathfinder::reEvaluateIfNeeded( const Heroes & hero )
|
||||
return result;
|
||||
}();
|
||||
|
||||
auto currentSettings = std::tie( _pathStart, _color, _remainingMovePoints, _maxMovePoints, _pathfindingSkill, _armyStrength, _isArtifactsBagFull,
|
||||
_isSummonBoatSpellAvailable, _townGateCastleIndex, _townPortalCastleIndexes );
|
||||
const auto newSettings = std::make_tuple( hero.GetIndex(), hero.GetColor(), hero.GetMovePoints(), hero.GetMaxMovePoints(),
|
||||
static_cast<uint8_t>( hero.GetLevelSkill( Skill::Secondary::PATHFINDING ) ), hero.GetArmy().GetStrength(),
|
||||
hero.IsFullBagArtifacts(), isSummonBoatSpellAvailable, townGateCastleIndex, townPortalCastleIndexes );
|
||||
auto currentSettings = std::tie( _pathStart, _color, _remainingMovePoints, _pathfindingSkill, _maxMovePointsOnLand, _maxMovePointsOnWater, _armyStrength,
|
||||
_isArtifactsBagFull, _isSummonBoatSpellAvailable, _townGateCastleIndex, _townPortalCastleIndexes );
|
||||
const auto newSettings
|
||||
= std::make_tuple( hero.GetIndex(), hero.GetColor(), hero.GetMovePoints(), static_cast<uint8_t>( hero.GetLevelSkill( Skill::Secondary::PATHFINDING ) ),
|
||||
hero.GetMaxMovePoints( false ), hero.GetMaxMovePoints( true ), hero.GetArmy().GetStrength(), hero.IsFullBagArtifacts(),
|
||||
isSummonBoatSpellAvailable, townGateCastleIndex, townPortalCastleIndexes );
|
||||
|
||||
if ( currentSettings != newSettings ) {
|
||||
currentSettings = newSettings;
|
||||
@@ -670,9 +678,9 @@ void AIWorldPathfinder::reEvaluateIfNeeded( const Heroes & hero )
|
||||
|
||||
void AIWorldPathfinder::reEvaluateIfNeeded( const int start, const int color, const double armyStrength, const uint8_t skill )
|
||||
{
|
||||
auto currentSettings = std::tie( _pathStart, _color, _remainingMovePoints, _maxMovePoints, _pathfindingSkill, _armyStrength, _isArtifactsBagFull,
|
||||
_isSummonBoatSpellAvailable, _townGateCastleIndex, _townPortalCastleIndexes );
|
||||
const auto newSettings = std::make_tuple( start, color, 0U, 0U, skill, armyStrength, false, false, -1, std::vector<int32_t>{} );
|
||||
auto currentSettings = std::tie( _pathStart, _color, _remainingMovePoints, _pathfindingSkill, _maxMovePointsOnLand, _maxMovePointsOnWater, _armyStrength,
|
||||
_isArtifactsBagFull, _isSummonBoatSpellAvailable, _townGateCastleIndex, _townPortalCastleIndexes );
|
||||
const auto newSettings = std::make_tuple( start, color, 0U, skill, 0U, 0U, armyStrength, false, false, -1, std::vector<int32_t>{} );
|
||||
|
||||
if ( currentSettings != newSettings ) {
|
||||
currentSettings = newSettings;
|
||||
@@ -683,10 +691,12 @@ void AIWorldPathfinder::reEvaluateIfNeeded( const int start, const int color, co
|
||||
|
||||
void AIWorldPathfinder::processWorldMap()
|
||||
{
|
||||
// reset cache back to default value
|
||||
for ( auto & node : _cache ) {
|
||||
node.resetNode();
|
||||
assert( _cache.size() == world.getSize() && Maps::isValidAbsIndex( _pathStart ) );
|
||||
|
||||
for ( WorldNode & node : _cache ) {
|
||||
node.reset();
|
||||
}
|
||||
|
||||
_cache[_pathStart] = WorldNode( -1, 0, MP2::OBJ_NONE, _remainingMovePoints );
|
||||
|
||||
std::vector<int> nodesToExplore;
|
||||
@@ -734,7 +744,7 @@ void AIWorldPathfinder::processCurrentNode( std::vector<int> & nodesToExplore, c
|
||||
if ( !isFirstNode ) {
|
||||
if ( !isTileAccessibleForAIWithArmy( currentNodeIdx, _armyStrength, _minimalArmyStrengthAdvantage ) ) {
|
||||
// If we can't move here, then reset the node
|
||||
currentNode.resetNode();
|
||||
currentNode.reset();
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -788,16 +798,23 @@ void AIWorldPathfinder::processCurrentNode( std::vector<int> & nodesToExplore, c
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t AIWorldPathfinder::getMaxMovePoints( const bool onWater ) const
|
||||
{
|
||||
return onWater ? _maxMovePointsOnWater : _maxMovePointsOnLand;
|
||||
}
|
||||
|
||||
uint32_t AIWorldPathfinder::getMovementPenalty( const int from, const int to, const int direction ) const
|
||||
{
|
||||
const uint32_t defaultPenalty = [this, from, to, direction]() {
|
||||
const Maps::Tiles & fromTile = world.GetTiles( from );
|
||||
|
||||
const uint32_t defaultPenalty = [this, from, to, direction, &fromTile]() {
|
||||
const uint32_t regularPenalty = WorldPathfinder::getMovementPenalty( from, to, direction );
|
||||
|
||||
if ( from == _pathStart ) {
|
||||
return regularPenalty;
|
||||
}
|
||||
|
||||
const MP2::MapObjectType objectType = world.GetTiles( from ).GetObject();
|
||||
const MP2::MapObjectType objectType = fromTile.GetObject();
|
||||
if ( !MP2::isNeedStayFront( objectType ) || objectType == MP2::OBJ_BOAT ) {
|
||||
return regularPenalty;
|
||||
}
|
||||
@@ -818,24 +835,25 @@ uint32_t AIWorldPathfinder::getMovementPenalty( const int from, const int to, co
|
||||
// According to a rough estimate, the movement points spent can be considered the same in both cases,
|
||||
// therefore, we apply an additional penalty when moving from the tile containing this object to the
|
||||
// next tile. In general, it is impossible to perform an accurate estimation, since the stats and
|
||||
// skills of a moving hero may change after interacting with the object.
|
||||
// skills of a moving hero may change after interacting with the object (e.g. hero can upgrade his
|
||||
// Pathfinding skill).
|
||||
//
|
||||
// The real path will not reach this step, so this logic will be used to estimate distances more
|
||||
// accurately when choosing whether to move through objects or past them.
|
||||
return regularPenalty + WorldPathfinder::getMovementPenalty( node._from, from, prevStepDirection );
|
||||
}();
|
||||
|
||||
assert( _maxMovePoints == 0 || defaultPenalty <= _maxMovePoints );
|
||||
const uint32_t maxMovePoints = getMaxMovePoints( fromTile.isWater() );
|
||||
assert( maxMovePoints == 0 || defaultPenalty <= maxMovePoints );
|
||||
|
||||
// If we perform pathfinding for a real AI-controlled hero on the map, we should correctly calculate
|
||||
// movement penalties when this hero overcomes water obstacles using boats.
|
||||
if ( _maxMovePoints > 0 ) {
|
||||
if ( maxMovePoints > 0 ) {
|
||||
const WorldNode & node = _cache[from];
|
||||
|
||||
// No dead ends allowed
|
||||
assert( from == _pathStart || node._from != -1 );
|
||||
|
||||
const Maps::Tiles & fromTile = world.GetTiles( from );
|
||||
const Maps::Tiles & toTile = world.GetTiles( to );
|
||||
|
||||
// AI-controlled hero may get from the shore to an empty water tile using the Summon Boat spell
|
||||
@@ -848,7 +866,7 @@ uint32_t AIWorldPathfinder::getMovementPenalty( const int from, const int to, co
|
||||
// If the hero is not able to make this movement this turn, then he will have to spend
|
||||
// all the movement points next turn.
|
||||
if ( defaultPenalty > node._remainingMovePoints ) {
|
||||
return _maxMovePoints;
|
||||
return maxMovePoints;
|
||||
}
|
||||
|
||||
return node._remainingMovePoints;
|
||||
@@ -1162,9 +1180,9 @@ bool AIWorldPathfinder::isHeroPossiblyBlockingWay( const Heroes & hero )
|
||||
return heroTile.GetObject( false ) == MP2::OBJ_STONE_LITHS;
|
||||
}
|
||||
|
||||
std::vector<IndexObject> AIWorldPathfinder::getObjectsOnTheWay( const int targetIndex, const bool checkAdjacent /* = false */ ) const
|
||||
std::vector<IndexObject> AIWorldPathfinder::getObjectsOnTheWay( const int targetIndex ) const
|
||||
{
|
||||
assert( _pathStart != -1 && _color != Color::NONE && targetIndex != -1 );
|
||||
assert( _cache.size() == world.getSize() && Maps::isValidAbsIndex( _pathStart ) && _color != Color::NONE && Maps::isValidAbsIndex( targetIndex ) );
|
||||
|
||||
std::vector<IndexObject> result;
|
||||
|
||||
@@ -1174,9 +1192,8 @@ std::vector<IndexObject> AIWorldPathfinder::getObjectsOnTheWay( const int target
|
||||
}
|
||||
|
||||
const Kingdom & kingdom = world.GetKingdom( _color );
|
||||
const Directions & directions = Direction::All();
|
||||
|
||||
std::set<int> uniqueIndices;
|
||||
|
||||
const auto validateAndAdd = [&kingdom, &result, &uniqueIndices]( int index, const MP2::MapObjectType objectType ) {
|
||||
// std::set insert returns a pair, second value is true if it was unique
|
||||
if ( uniqueIndices.insert( index ).second && kingdom.isValidKingdomObject( world.GetTiles( index ), objectType ) ) {
|
||||
@@ -1202,20 +1219,6 @@ std::vector<IndexObject> AIWorldPathfinder::getObjectsOnTheWay( const int target
|
||||
|
||||
validateAndAdd( currentNode, node._objectID );
|
||||
|
||||
if ( checkAdjacent ) {
|
||||
for ( size_t i = 0; i < directions.size(); ++i ) {
|
||||
if ( Maps::isValidDirection( currentNode, directions[i] ) ) {
|
||||
const int newIndex = currentNode + _mapOffset[i];
|
||||
const WorldNode & adjacent = _cache[newIndex];
|
||||
|
||||
if ( adjacent._cost == 0 || adjacent._objectID == 0 )
|
||||
continue;
|
||||
|
||||
validateAndAdd( newIndex, adjacent._objectID );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The path should not pass through the same tile more than once
|
||||
assert( uniqPathIndexes.insert( node._from ).second );
|
||||
|
||||
@@ -1347,7 +1350,7 @@ std::list<Route::Step> AIWorldPathfinder::getDimensionDoorPath( const Heroes & h
|
||||
|
||||
std::list<Route::Step> AIWorldPathfinder::buildPath( const int targetIndex ) const
|
||||
{
|
||||
assert( _pathStart != -1 && targetIndex != -1 );
|
||||
assert( _cache.size() == world.getSize() && Maps::isValidAbsIndex( _pathStart ) && Maps::isValidAbsIndex( targetIndex ) );
|
||||
|
||||
std::list<Route::Step> path;
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
#include "color.h"
|
||||
#include "mp2.h"
|
||||
#include "pathfinding.h"
|
||||
#include "skill.h"
|
||||
|
||||
class Heroes;
|
||||
@@ -37,49 +36,45 @@ namespace Route
|
||||
class Step;
|
||||
}
|
||||
|
||||
struct WorldNode : public PathfindingNode<MP2::MapObjectType>
|
||||
struct WorldNode final
|
||||
{
|
||||
int _from{ -1 };
|
||||
uint32_t _cost{ 0 };
|
||||
MP2::MapObjectType _objectID{ MP2::OBJ_NONE };
|
||||
// The number of movement points remaining for the hero after moving to this node
|
||||
uint32_t _remainingMovePoints = 0;
|
||||
uint32_t _remainingMovePoints{ 0 };
|
||||
|
||||
WorldNode() = default;
|
||||
|
||||
WorldNode( const int node, const uint32_t cost, const MP2::MapObjectType object, const uint32_t remainingMovePoints )
|
||||
: PathfindingNode( node, cost, object )
|
||||
: _from( node )
|
||||
, _cost( cost )
|
||||
, _objectID( object )
|
||||
, _remainingMovePoints( remainingMovePoints )
|
||||
{}
|
||||
|
||||
WorldNode( const WorldNode & ) = delete;
|
||||
WorldNode( WorldNode && ) = default;
|
||||
|
||||
~WorldNode() override = default;
|
||||
|
||||
WorldNode & operator=( const WorldNode & ) = delete;
|
||||
WorldNode & operator=( WorldNode && ) = default;
|
||||
|
||||
void resetNode() override
|
||||
void reset()
|
||||
{
|
||||
PathfindingNode::resetNode();
|
||||
|
||||
_from = -1;
|
||||
_cost = 0;
|
||||
_objectID = MP2::OBJ_NONE;
|
||||
_remainingMovePoints = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Abstract class that provides base functionality to path through World map
|
||||
class WorldPathfinder : public Pathfinder<WorldNode>
|
||||
// Abstract class that provides basic functionality for navigating the World Map
|
||||
class WorldPathfinder
|
||||
{
|
||||
public:
|
||||
WorldPathfinder() = default;
|
||||
WorldPathfinder( const WorldPathfinder & ) = delete;
|
||||
|
||||
~WorldPathfinder() override = default;
|
||||
virtual ~WorldPathfinder() = default;
|
||||
|
||||
WorldPathfinder & operator=( const WorldPathfinder & ) = delete;
|
||||
|
||||
// This method resizes the cache and re-calculates map offsets if values are out of sync with World class
|
||||
virtual void checkWorldSize();
|
||||
virtual void reset();
|
||||
|
||||
static uint32_t calculatePathPenalty( const std::list<Route::Step> & path );
|
||||
uint32_t getDistance( int targetIndex ) const;
|
||||
|
||||
protected:
|
||||
virtual void processWorldMap();
|
||||
@@ -89,9 +84,13 @@ protected:
|
||||
// can be overridden by a derived class.
|
||||
virtual bool isMovementAllowed( const int from, const int direction ) const;
|
||||
|
||||
// This method defines pathfinding rules. This has to be implemented by the derived class.
|
||||
// Defines the pathfinding rules and should be implemented by a derived class.
|
||||
virtual void processCurrentNode( std::vector<int> & nodesToExplore, const int currentNodeIdx ) = 0;
|
||||
|
||||
// Returns the maximum number of movement points, depending on whether the movement is performed by land or by
|
||||
// water. Should be implemented by a derived class.
|
||||
virtual uint32_t getMaxMovePoints( const bool onWater ) const = 0;
|
||||
|
||||
// Calculates the movement penalty when moving from the source tile to the adjacent destination tile in the
|
||||
// specified direction. If the "last move" logic should be taken into account (when performing pathfinding
|
||||
// for a real hero on the map), then the source tile should be already accessible for this hero and it should
|
||||
@@ -99,18 +98,16 @@ protected:
|
||||
// overridden by a derived class.
|
||||
virtual uint32_t getMovementPenalty( const int from, const int to, const int direction ) const;
|
||||
|
||||
// Subtracts movement points taking the transition between turns into account
|
||||
uint32_t subtractMovePoints( const uint32_t movePoints, const uint32_t subtractedMovePoints ) const;
|
||||
|
||||
std::vector<WorldNode> _cache;
|
||||
std::vector<int> _mapOffset;
|
||||
|
||||
// Hero properties should be cached here because they can change even if the hero's position does not change,
|
||||
// so it should be possible to compare the old values with the new ones to detect the need to recalculate the
|
||||
// pathfinder's cache
|
||||
int _color = Color::NONE;
|
||||
uint32_t _remainingMovePoints = 0;
|
||||
uint32_t _maxMovePoints = 0;
|
||||
uint8_t _pathfindingSkill = Skill::Level::EXPERT;
|
||||
int _pathStart{ -1 };
|
||||
int _color{ Color::NONE };
|
||||
uint32_t _remainingMovePoints{ 0 };
|
||||
uint8_t _pathfindingSkill{ Skill::Level::EXPERT };
|
||||
};
|
||||
|
||||
class PlayerWorldPathfinder final : public WorldPathfinder
|
||||
@@ -134,6 +131,16 @@ public:
|
||||
private:
|
||||
// Follows regular passability rules (for the human player)
|
||||
void processCurrentNode( std::vector<int> & nodesToExplore, const int currentNodeIdx ) override;
|
||||
|
||||
// Returns the maximum number of movement points. This class is not intended for planning paths passing both on
|
||||
// land and on water at the same time, so the maximum number of movement points corresponding to the type of
|
||||
// surface on which the hero is currently located is used.
|
||||
uint32_t getMaxMovePoints( const bool onWater ) const override;
|
||||
|
||||
// Hero properties should be cached here because they can change even if the hero's position does not change,
|
||||
// so it should be possible to compare the old values with the new ones to detect the need to recalculate the
|
||||
// pathfinder's cache
|
||||
uint32_t _maxMovePoints{ 0 };
|
||||
};
|
||||
|
||||
class AIWorldPathfinder final : public WorldPathfinder
|
||||
@@ -161,7 +168,7 @@ public:
|
||||
|
||||
static bool isHeroPossiblyBlockingWay( const Heroes & hero );
|
||||
|
||||
std::vector<IndexObject> getObjectsOnTheWay( const int targetIndex, const bool checkAdjacent = false ) const;
|
||||
std::vector<IndexObject> getObjectsOnTheWay( const int targetIndex ) const;
|
||||
|
||||
std::list<Route::Step> getDimensionDoorPath( const Heroes & hero, int targetIndex ) const;
|
||||
|
||||
@@ -173,7 +180,7 @@ public:
|
||||
// Used for non-hero armies, like castles or monsters
|
||||
uint32_t getDistance( int start, int targetIndex, int color, double armyStrength, uint8_t skill = Skill::Level::EXPERT );
|
||||
// Faster, but does not re-evaluate the map (exposed method of the base class)
|
||||
using Pathfinder::getDistance;
|
||||
using WorldPathfinder::getDistance;
|
||||
|
||||
// Returns the coefficient of the minimum required advantage in army strength in order to be able to "pass through"
|
||||
// protected tiles from the AI pathfinder's point of view
|
||||
@@ -206,6 +213,10 @@ private:
|
||||
// Follows custom passability rules (for the AI)
|
||||
void processCurrentNode( std::vector<int> & nodesToExplore, const int currentNodeIdx ) override;
|
||||
|
||||
// Returns the maximum number of movement points, depending on whether the movement is performed by land or by
|
||||
// water
|
||||
uint32_t getMaxMovePoints( const bool onWater ) const override;
|
||||
|
||||
// Adds special logic for AI-controlled heroes to correctly calculate movement penalties when such a hero passes
|
||||
// through objects on the map or overcomes water obstacles using boats. If this logic should be taken into account
|
||||
// (when performing pathfinding for a real hero on the map), then the source tile should be already accessible for
|
||||
@@ -215,6 +226,8 @@ private:
|
||||
// Hero properties should be cached here because they can change even if the hero's position does not change,
|
||||
// so it should be possible to compare the old values with the new ones to detect the need to recalculate the
|
||||
// pathfinder's cache
|
||||
uint32_t _maxMovePointsOnLand{ 0 };
|
||||
uint32_t _maxMovePointsOnWater{ 0 };
|
||||
double _armyStrength{ -1 };
|
||||
bool _isArtifactsBagFull{ false };
|
||||
bool _isSummonBoatSpellAvailable{ false };
|
||||
|
||||
Reference in New Issue
Block a user