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 creature pathfinding, and unify monster pathfinding logic. #70274

Closed
wants to merge 47 commits into from
Closed
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d1e2bc6
Refactor creature pathfinding, and unify monster pathfinding logic.
prharvey Dec 18, 2023
77fd1be
Maybe fix broken build. Not sure why this builds locally with the hea…
prharvey Dec 18, 2023
fb1c7fd
Fix more issues older compilers do not like.
prharvey Dec 18, 2023
5d2b4fb
Remove unused function z_is_valid().
prharvey Dec 18, 2023
376ded4
Clang-tidy fixes.
prharvey Dec 18, 2023
4f5337d
Remove the packed tripoint. It was helpful in earlier versions, but i…
prharvey Dec 18, 2023
b43250e
Clean up the next node/neighbor function using tripoint functions. Ma…
prharvey Dec 18, 2023
5fafef0
Clang-tidy fixes and general clean up.
prharvey Dec 18, 2023
0daf69c
Move some operations to points to make clang-tidy happy.
prharvey Dec 19, 2023
8a0f454
Remove some leftover instrumentation code.
prharvey Dec 19, 2023
22b8bae
Move the distance functions into tripoint code, and clean up the stra…
prharvey Dec 19, 2023
ade38ad
Move the A* implementation into its own header file.
prharvey Dec 19, 2023
83359b6
Fix misleading documentation on Climmable flag.
prharvey Dec 19, 2023
f384d91
Revert changes to maptile_impl, as they are no longer needed.
prharvey Dec 19, 2023
b8f389e
Add missing <limits> header.
prharvey Dec 20, 2023
ca9b9cc
Add test infrastructure and some simple cases.
prharvey Dec 29, 2023
afcfcdd
Add 2D test cases for avoidance/allowance, and fix some bugs found.
prharvey Dec 29, 2023
6a25cc2
Add support for 3D tests, and add some.
prharvey Dec 30, 2023
6b67e4a
Clean up some test cases so they are easier to read.
prharvey Dec 30, 2023
06dfc82
Add support for monster tests, and add a test using a mi-go.
prharvey Dec 30, 2023
182b52b
Add tests for mi-go traversing ramps and opening doors. Fixed some bugs.
prharvey Dec 30, 2023
7b1cc97
astyle map.h after rebase
prharvey Dec 30, 2023
34e2d24
Invalidate bad paths after moving to handle the case where we were fo…
prharvey Dec 30, 2023
e9bdd16
Use fast bitset test.
prharvey Dec 30, 2023
dc92feb
Invalidate bad monster paths, and some assorted microoptimizations.
prharvey Dec 30, 2023
83a6ba6
Some micro-optimizations.
prharvey Dec 30, 2023
3fb80e5
Add exponential backoff for stuck monsters using intelligent pathfind…
prharvey Dec 31, 2023
895c1a9
Add more monster tests.
prharvey Dec 31, 2023
9c5b5ef
clang-tidy fixes
prharvey Dec 31, 2023
ccf92b1
Replace the test landmines with dissectors. The explosions were break…
prharvey Dec 31, 2023
d8ab151
Add a flying mi-go, and use it to test flying monster pathfinding.
prharvey Dec 31, 2023
8292f5d
Change the flying mi-go name to "mi-go shrike" and add that to the di…
prharvey Dec 31, 2023
e8a7411
Add the plural as well.
prharvey Dec 31, 2023
7cb75a8
Fix styling in original mi-go json.
prharvey Dec 31, 2023
34af5b0
Add more test coverage for flying monsters, and fix bugs found.
prharvey Dec 31, 2023
15e9801
Add dangerous field tests.
prharvey Dec 31, 2023
d1e669d
Add test cases that force movement directly away from the objective.
prharvey Dec 31, 2023
4f2f1ec
Added a test for digging under burrowable walls.
prharvey Jan 1, 2024
6c23506
Integrate new monster vehicle movement + fixes in #70560.
prharvey Jan 1, 2024
9561552
Add support for tile size restrictions, and add tests for it. Also ad…
prharvey Jan 1, 2024
3296c70
Add support for vehicles in tests, and add some basic ones.
prharvey Jan 1, 2024
b423e57
Add tests for impassable but bashable vehicles.
prharvey Jan 1, 2024
82dc943
Add missing default case.
prharvey Jan 1, 2024
049dbd3
Refactor size resitrictions, and take the minumum cost option when en…
prharvey Jan 1, 2024
e6649a0
Fix some bugs in the minimum cost tile logic, and simplify the interf…
prharvey Jan 2, 2024
420802e
Only initialize the cache for z levels we need to use. Removes stutte…
prharvey Jan 2, 2024
4221494
Merge
prharvey Jan 2, 2024
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
78 changes: 77 additions & 1 deletion data/json/monsters/mi-go.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,81 @@
"melee_skill": 5,
"melee_dice": 4,
"melee_dice_sides": 6,
"melee_damage": [
{
"damage_type": "cut",
"amount": 6
}
],
prharvey marked this conversation as resolved.
Show resolved Hide resolved
"dodge": 4,
"bleed_rate": 75,
"vision_day": 50,
"vision_night": 20,
"weakpoint_sets": [ "wps_mi-go", "wps_natural_armor" ],
"families": [ "prof_wp_mi-go_basic", "prof_wp_mi-go_advanced", "prof_wp_nat_armored" ],
"harvest": "mi-go",
"path_settings": {
"max_dist": 50,
"allow_open_doors": true,
"avoid_traps": true,
"avoid_sharp": true
},
"scents_ignored": [ "sc_fetid" ],
"special_attacks": [
[ "PARROT", 100 ],
{
"id": "scratch",
"damage_max_instance": [
{
"damage_type": "cut",
"amount": 23,
"armor_multiplier": 0.8
}
]
}
],
prharvey marked this conversation as resolved.
Show resolved Hide resolved
"flags": [
"SEES",
"SMELLS",
"HEARS",
"WARM",
"HAS_MIND",
"BASHES",
"POISON",
"NO_BREATHE",
"ARTHROPOD_BLOOD",
"PATH_AVOID_DANGER_1",
"CAN_OPEN_DOORS",
"PRIORITIZE_TARGETS",
"CORNERED_FIGHTER"
],
"armor": {
"bash": 4,
"cut": 12,
"bullet": 10,
"electric": 2
}
prharvey marked this conversation as resolved.
Show resolved Hide resolved
},
{
"id": "mon_mi_go_flying",
"type": "MONSTER",
"name": { "str": "mi-go shrike" },
"description": "An alien creature of uncertain origin. Its shapeless pink body bears numerous sets of paired appendages of unknown function, and a pair of ribbed, membranous wings which allow it to take to the air. Its odd, vaguely pyramid-shaped head bristles with numerous wavering antennae, and it moves with an uncanny fluidity on its many legs.",
"default_faction": "mi-go",
"bodytype": "migo",
"species": [ "MIGO" ],
"volume": "92500 ml",
"weight": "80 kg",
"hp": 180,
"speed": 120,
"material": [ "mi-go_flesh" ],
"symbol": "&",
"color": "pink",
"aggression": 20,
"morale": 30,
"melee_skill": 5,
"melee_dice": 4,
"melee_dice_sides": 6,
"melee_damage": [ { "damage_type": "cut", "amount": 6 } ],
"dodge": 4,
"bleed_rate": 75,
Expand Down Expand Up @@ -46,7 +121,8 @@
"PATH_AVOID_DANGER_1",
"CAN_OPEN_DOORS",
"PRIORITIZE_TARGETS",
"CORNERED_FIGHTER"
"CORNERED_FIGHTER",
"FLIES"
],
"armor": { "bash": 4, "cut": 12, "bullet": 10, "electric": 2 }
},
Expand Down
2 changes: 1 addition & 1 deletion data/json/monsters/zed_amalgamation.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"families": [ "prof_intro_biology", "prof_physiology", "prof_wp_amalgamation" ],
"harvest": "zombie_amalgamation",
"//": "TODO: When effects/spells can mess with path settings + flags move smartness to the effect of dedicated coordinatiors",
"path_settings": { "avoid_sharp": true, "avoid_traps": true, "max_dist": 400 },
"path_settings": { "avoid_sharp": true, "avoid_traps": true, "max_dist": 40 },
"flags": [ "SEES", "SMELLS", "HEARS", "PATH_AVOID_DANGER_2", "REVIVES", "NO_BREATHE" ],
"armor": { "bash": 8, "cut": 4, "stab": 4, "acid": 5, "bullet": 12, "electric": 5 }
},
Expand Down
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
7 changes: 5 additions & 2 deletions src/character.h
Original file line number Diff line number Diff line change
Expand Up @@ -3171,8 +3171,11 @@ 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::set<tripoint> get_path_avoid() const override;
/** Returns settings for pathfinding. */
virtual const pathfinding_settings &get_pathfinding_settings() const;
/** Returns a set of points we do not want to path through. */
virtual std::set<tripoint> get_path_avoid() const;

/**
* Get all hostile creatures currently visible to this player.
*/
Expand Down
Loading
Loading