Improve path calculations by AIWorldPathfinder in planning mode (#7739)

This commit is contained in:
Oleg Derevenetz
2023-09-14 10:49:05 +08:00
committed by GitHub
parent 871453d3b9
commit 269ea509b2
10 changed files with 207 additions and 251 deletions
-1
View File
@@ -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" />
-78
View File
@@ -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;
};
+4 -4
View File
@@ -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 )
+8 -8
View File
@@ -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;
};
+40 -33
View File
@@ -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
+4
View File
@@ -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
{
+6
View File
@@ -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(); } );
}
+2
View File
@@ -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
+98 -95
View File
@@ -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;
+45 -32
View File
@@ -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 };