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

Refactor pathfinding. #75945

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions src/a_star.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
#pragma once
#ifndef CATA_SRC_A_STAR_H
#define CATA_SRC_A_STAR_H

#include <algorithm>
#include <limits>
#include <optional>
#include <queue>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

template <typename Node, typename Cost = int, typename VisitedSet = std::unordered_set<Node>, typename BestPathMap = std::unordered_map<Node, std::pair<Cost, Node>>>
class AStarState
{
public:
void reset( const Node &start, Cost cost );

std::optional<Node> get_next( Cost max );

template <typename StateCostFn, typename TransitionCostFn, typename HeuristicFn, typename NeighborsFn>
void generate_neighbors( const Node &current, StateCostFn &&s_cost_fn, TransitionCostFn &&t_cost_fn,
HeuristicFn &&heuristic_fn, NeighborsFn &&neighbors_fn );

bool has_path_to( const Node &end ) const;

Cost path_cost( const Node &end ) const;

std::vector<Node> path_to( const Node &end ) const;

private:
struct FirstElementGreaterThan {
template <typename T, typename... Ts>
bool operator()( const std::tuple<T, Ts...> &lhs, const std::tuple<T, Ts...> &rhs ) const {
return std::get<0>( lhs ) > std::get<0>( rhs );
}
};

using FrontierNode = std::tuple<Cost, Node>;
using FrontierQueue =
std::priority_queue<FrontierNode, std::vector<FrontierNode>, FirstElementGreaterThan>;

VisitedSet visited_;
BestPathMap best_paths_;
FrontierQueue frontier_;
};

template <typename Node, typename Cost = int, typename VisitedSet = std::unordered_set<Node>, typename BestStateMap = std::unordered_map<Node, std::pair<Cost, Node>>>
class AStarPathfinder
{
public:
template <typename StateCostFn, typename TransitionCostFn, typename HeuristicFn, typename NeighborsFn>
std::vector<Node> find_path( Cost max, const Node &from, const Node &to,
StateCostFn &&s_cost_fn, TransitionCostFn &&t_cost_fn, HeuristicFn &&heuristic_fn,
NeighborsFn &&neighbors_fn );

private:
AStarState<Node, Cost, VisitedSet, BestStateMap> state_;
};

template <typename Node, typename Cost = int, typename VisitedSet = std::unordered_set<Node>, typename BestStateMap = std::unordered_map<Node, std::pair<Cost, Node>>>
class BidirectionalAStarPathfinder
{
public:
template <typename StateCostFn, typename TransitionCostFn, typename HeuristicFn, typename NeighborsFn>
std::vector<Node> find_path( Cost max, const Node &from, const Node &to,
StateCostFn &&s_cost_fn, TransitionCostFn &&t_cost_fn, HeuristicFn &&heuristic_fn,
NeighborsFn &&neighbors_fn );

private:
AStarState<Node, Cost, VisitedSet, BestStateMap> forward_;
AStarState<Node, Cost, VisitedSet, BestStateMap> backward_;
};

// Implementation Details

template <typename Node, typename Cost, typename VisitedSet, typename BestPathMap>
void AStarState<Node, Cost, VisitedSet, BestPathMap>::reset( const Node &start, Cost cost )
{
visited_.clear();
best_paths_.clear();

// priority_queue doesn't have a clear method, so we cannot reuse it, and it may
// get quite large, so we explicitly create the underlying container and reserve
// space beforehand.
std::vector<FrontierNode> queue_data;
queue_data.reserve( 400 );
frontier_ = FrontierQueue( FirstElementGreaterThan(), std::move( queue_data ) );

best_paths_.try_emplace( start, 0, start );
frontier_.emplace( cost, start );
}

template <typename Node, typename Cost, typename VisitedSet, typename BestPathMap>
bool AStarState<Node, Cost, VisitedSet, BestPathMap>::has_path_to( const Node &end ) const
{
return best_paths_.count( end );
}

template <typename Node, typename Cost, typename VisitedSet, typename BestPathMap>
Cost AStarState<Node, Cost, VisitedSet, BestPathMap>::path_cost( const Node &end ) const
{
return best_paths_.at( end ).first;
}

template <typename Node, typename Cost, typename VisitedSet, typename BestPathMap>
std::vector<Node> AStarState<Node, Cost, VisitedSet, BestPathMap>::path_to( const Node &end ) const
{
std::vector<Node> result;
if( has_path_to( end ) ) {
Node current = end;
while( best_paths_.at( current ).first != 0 ) {
result.push_back( current );
current = best_paths_.at( current ).second;
}
std::reverse( result.begin(), result.end() );
}
return result;
}

template <typename Node, typename Cost, typename VisitedSet, typename BestPathMap>
std::optional<Node> AStarState<Node, Cost, VisitedSet, BestPathMap>::get_next( Cost max )
{
while( !frontier_.empty() ) {
auto [cost, current] = frontier_.top();
frontier_.pop();

if( cost >= max ) {
return std::nullopt;
}

if( const auto& [_, inserted] = visited_.emplace( current ); !inserted ) {
continue;
}
return current;
}
return std::nullopt;
}

template <typename Node, typename Cost, typename VisitedSet, typename BestPathMap>
template <typename StateCostFn, typename TransitionCostFn, typename HeuristicFn, typename NeighborsFn>
void AStarState<Node, Cost, VisitedSet, BestPathMap>::generate_neighbors(
const Node &current, StateCostFn &&s_cost_fn, TransitionCostFn &&t_cost_fn,
HeuristicFn &&heuristic_fn,
NeighborsFn &&neighbors_fn )
{
// Can't use structured bindings here due to a defect in Clang 10.
const std::pair<Cost, Node> &best_path = best_paths_[current];
const Cost current_cost = best_path.first;
const Node &current_parent = best_path.second;
neighbors_fn( current_parent, current, [this, &s_cost_fn, &t_cost_fn, &heuristic_fn, &current,
current_cost]( const Node & neighbour ) {
if( visited_.count( neighbour ) ) {
return;
}
if( const std::optional<Cost> s_cost = s_cost_fn( neighbour ) ) {
if( const std::optional<Cost> t_cost = t_cost_fn( current, neighbour ) ) {
const auto& [iter, _] = best_paths_.try_emplace( neighbour, std::numeric_limits<Cost>::max(),
Node() );
auto& [best_cost, parent] = *iter;
const Cost new_cost = current_cost + *s_cost + *t_cost;
if( new_cost < best_cost ) {
best_cost = new_cost;
parent = current;
const Cost estimated_cost = new_cost + heuristic_fn( neighbour );
frontier_.emplace( estimated_cost, neighbour );
}
}
} else {
visited_.emplace( neighbour );
}
} );
}

template <typename Node, typename Cost, typename VisitedSet, typename BestStateMap>
template <typename StateCostFn, typename TransitionCostFn, typename HeuristicFn, typename NeighborsFn>
std::vector<Node> AStarPathfinder<Node, Cost, VisitedSet, BestStateMap>::find_path(
Cost max_cost, const Node &from, const Node &to, StateCostFn &&s_cost_fn,
TransitionCostFn &&t_cost_fn,
HeuristicFn &&heuristic_fn, NeighborsFn &&neighbors_fn )
{
if( !s_cost_fn( from ) || !s_cost_fn( to ) ) {
return {};
}

state_.reset( from, heuristic_fn( from, to ) );
while( const std::optional<Node> current = state_.get_next( max_cost ) ) {
if( *current == to ) {
return state_.path_to( to );
}
state_.generate_neighbors( *current, s_cost_fn, t_cost_fn, [&heuristic_fn,
to]( const Node & from ) {
return heuristic_fn( from, to );
}, neighbors_fn );
}
return {};
}


template <typename Node, typename Cost, typename VisitedSet, typename BestStateMap>
template <typename StateCostFn, typename TransitionCostFn, typename HeuristicFn, typename NeighborsFn>
std::vector<Node> BidirectionalAStarPathfinder<Node, Cost, VisitedSet, BestStateMap>::find_path(
Cost max_cost, const Node &from, const Node &to, StateCostFn &&s_cost_fn,
TransitionCostFn &&t_cost_fn,
HeuristicFn &&heuristic_fn, NeighborsFn &&neighbors_fn )
{
if( !s_cost_fn( from ) || !s_cost_fn( to ) ) {
return {};
}

// The full cost is not used since that would result in paths that are up to 2x longer than
// intended. Half the cost cannot be used, since there is no guarantee that both searches
// proceed at the same pace. 2/3rds the cost is a fine balance between the two, and has the
// effect of the worst case still visiting less states than normal A*.
const Cost partial_max_cost = 2 * max_cost / 3;
forward_.reset( from, heuristic_fn( from, to ) );
backward_.reset( to, heuristic_fn( to, from ) );
for( ;; ) {
const std::optional<Node> f_current_state = forward_.get_next( partial_max_cost );
if( !f_current_state ) {
break;
}
const std::optional<Node> b_current_state = backward_.get_next( partial_max_cost );
if( !b_current_state ) {
break;
}

bool f_links = backward_.has_path_to( *f_current_state );
bool b_links = forward_.has_path_to( *b_current_state );

if( f_links && b_links ) {
const Cost f_cost = forward_.path_cost( *f_current_state ) + backward_.path_cost(
*f_current_state );
const Cost b_cost = forward_.path_cost( *b_current_state ) + backward_.path_cost(
*b_current_state );
if( b_cost < f_cost ) {
f_links = false;
} else {
b_links = false;
}
}

if( f_links || b_links ) {
const Node &midpoint = f_links ? *f_current_state : *b_current_state;
std::vector<Node> forward_path = forward_.path_to( midpoint );
std::vector<Node> backward_path = backward_.path_to( midpoint );
if( backward_path.empty() ) {
return forward_path;
}
backward_path.pop_back();
std::for_each( backward_path.rbegin(), backward_path.rend(), [&forward_path]( const Node & node ) {
forward_path.push_back( node );
} );
forward_path.push_back( to );
return forward_path;
}

forward_.generate_neighbors( *f_current_state, s_cost_fn, t_cost_fn, [&heuristic_fn,
&to]( const Node & from ) {
return heuristic_fn( from, to );
}, neighbors_fn );
backward_.generate_neighbors( *b_current_state, s_cost_fn, [&t_cost_fn]( const Node & from,
const Node & to ) {
return t_cost_fn( to, from );
}, [&heuristic_fn, &from]( const Node & to ) {
return heuristic_fn( to, from );
}, neighbors_fn );
}
return {};
}

#endif // CATA_SRC_A_STAR_H
6 changes: 2 additions & 4 deletions src/activity_actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7188,8 +7188,7 @@ void unload_loot_activity_actor::do_turn( player_activity &act, Character &you )
return;
}
std::vector<tripoint_bub_ms> route;
route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings(),
you.get_path_avoid() );
route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings() );
if( route.empty() ) {
// can't get there, can't do anything, skip it
continue;
Expand Down Expand Up @@ -7225,8 +7224,7 @@ void unload_loot_activity_actor::do_turn( player_activity &act, Character &you )
// get either direct route or route to nearest adjacent tile if
// source tile is impassable
if( here.passable( src_loc ) ) {
route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings(),
you.get_path_avoid() );
route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings() );
} else {
// impassable source tile (locker etc.),
// get route to nearest adjacent tile instead
Expand Down
6 changes: 2 additions & 4 deletions src/activity_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2911,8 +2911,7 @@ void activity_handlers::travel_do_turn( player_activity *act, Character *you )
}
}
const std::vector<tripoint_bub_ms> route_to =
here.route( you->pos_bub(), centre_sub, you->get_pathfinding_settings(),
you->get_path_avoid() );
here.route( you->pos_bub(), centre_sub, you->get_pathfinding_settings() );
if( !route_to.empty() ) {
const activity_id act_travel = ACT_TRAVELLING;
you->set_destination( route_to, player_activity( act_travel ) );
Expand Down Expand Up @@ -3598,8 +3597,7 @@ static void perform_zone_activity_turn(
const tripoint_bub_ms &tile_loc = here.bub_from_abs( tile );

std::vector<tripoint_bub_ms> route =
here.route( you->pos_bub(), tile_loc, you->get_pathfinding_settings(),
you->get_path_avoid() );
here.route( you->pos_bub(), tile_loc, you->get_pathfinding_settings() );
if( route.size() > 1 ) {
route.pop_back();

Expand Down
10 changes: 4 additions & 6 deletions src/activity_item_handling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ std::vector<tripoint_bub_ms> route_adjacent( const Character &you, const tripoin
const auto &avoid = you.get_path_avoid();
for( const tripoint_bub_ms &tp : sorted ) {
std::vector<tripoint_bub_ms> route =
here.route( you.pos_bub(), tp, you.get_pathfinding_settings(), avoid );
here.route( you.pos_bub(), tp, you.get_pathfinding_settings() );

if( !route.empty() ) {
return route;
Expand Down Expand Up @@ -752,7 +752,7 @@ static std::vector<tripoint_bub_ms> route_best_workbench(
}
for( const tripoint_bub_ms &tp : sorted ) {
std::vector<tripoint_bub_ms> route =
here.route( you.pos_bub(), tp, you.get_pathfinding_settings(), avoid );
here.route( you.pos_bub(), tp, you.get_pathfinding_settings() );

if( !route.empty() ) {
return route;
Expand Down Expand Up @@ -2055,8 +2055,7 @@ void activity_on_turn_move_loot( player_activity &act, Character &you )
return;
}
std::vector<tripoint_bub_ms> route;
route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings(),
you.get_path_avoid() );
route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings() );
if( route.empty() ) {
// can't get there, can't do anything, skip it
continue;
Expand Down Expand Up @@ -2112,8 +2111,7 @@ void activity_on_turn_move_loot( player_activity &act, Character &you )
// get either direct route or route to nearest adjacent tile if
// source tile is impassable
if( here.passable( src_loc ) ) {
route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings(),
you.get_path_avoid() );
route = here.route( you.pos_bub(), src_loc, you.get_pathfinding_settings() );
} else {
// impassable source tile (locker etc.),
// get route to nearest adjacent tile instead
Expand Down
4 changes: 2 additions & 2 deletions src/character.h
Original file line number Diff line number Diff line change
Expand Up @@ -3264,8 +3264,8 @@ class Character : public Creature, public visitable
/** Returns the player's modified base movement cost */
int run_cost( int base_cost, bool diag = false ) const;

const pathfinding_settings &get_pathfinding_settings() const override;
std::function<bool( const tripoint & )> get_path_avoid() const override;
virtual const pathfinding_settings &get_pathfinding_settings() const;
virtual std::function<bool( const tripoint & )> get_path_avoid() const;
/**
* Get all hostile creatures currently visible to this player.
*/
Expand Down
7 changes: 7 additions & 0 deletions src/coordinates.h
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,13 @@ inline int octile_dist( const coords::coord_point<Point, Origin, Scale, LhsInBou
return octile_dist( loc1.raw(), loc2.raw(), multiplier );
}

template<typename Point, coords::origin Origin, coords::scale Scale>
inline float octile_dist_exact( const coords::coord_point<Point, Origin, Scale> &loc1,
const coords::coord_point<Point, Origin, Scale> &loc2 )
{
return octile_dist_exact( loc1.raw(), loc2.raw() );
}

template<typename Point, coords::origin Origin, coords::scale Scale, bool LhsInBounds, bool RhsInBounds>
direction direction_from( const coords::coord_point<Point, Origin, Scale, LhsInBounds> &loc1,
const coords::coord_point<Point, Origin, Scale, RhsInBounds> &loc2 )
Expand Down
5 changes: 0 additions & 5 deletions src/creature.h
Original file line number Diff line number Diff line change
Expand Up @@ -945,11 +945,6 @@ class Creature : public viewer

virtual units::mass weight_capacity() const;

/** Returns settings for pathfinding. */
virtual const pathfinding_settings &get_pathfinding_settings() const = 0;
/** Returns a set of points we do not want to path through. */
virtual std::function<bool( const tripoint & )> get_path_avoid() const = 0;

bool underwater;
void draw( const catacurses::window &w, const point_bub_ms &origin, bool inverted ) const;
// TODO: Get rid of the untyped overload
Expand Down
3 changes: 1 addition & 2 deletions src/debug_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3427,8 +3427,7 @@ static void set_automove()

// TODO: fix point types
auto rt = get_map().route( player_character.pos_bub(), tripoint_bub_ms( *dest ),
player_character.get_pathfinding_settings(),
player_character.get_path_avoid() );
player_character.get_pathfinding_settings() );
if( !rt.empty() ) {
player_character.set_destination( rt );
} else {
Expand Down
Loading
Loading