Compare commits
3 commits
2086d25581
...
3030ac488b
| Author | SHA1 | Date | |
|---|---|---|---|
| 3030ac488b | |||
| 17f2d6e1ef | |||
| 767d0d4b0f |
19 changed files with 1499 additions and 180 deletions
|
|
@ -2,9 +2,22 @@
|
|||
#include "UIEntity.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "PathProvider.h"
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
|
||||
// Out-of-line reset lives here so the header can forward-declare PathProvider.
|
||||
void EntityBehavior::reset() {
|
||||
type = BehaviorType::IDLE;
|
||||
waypoints.clear();
|
||||
current_waypoint_index = 0;
|
||||
patrol_direction = 1;
|
||||
current_path.clear();
|
||||
path_step_index = 0;
|
||||
sleep_turns_remaining = 0;
|
||||
path_provider.reset();
|
||||
}
|
||||
|
||||
// Thread-local random engine for behavior randomness
|
||||
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||
|
||||
|
|
@ -226,78 +239,28 @@ static BehaviorOutput executeSleep(UIEntity& entity, UIGrid& grid) {
|
|||
return {BehaviorResult::NO_ACTION, {}};
|
||||
}
|
||||
|
||||
static BehaviorOutput executeSeek(UIEntity& entity, UIGrid& grid) {
|
||||
// SEEK and FLEE share one implementation now: both delegate to the active
|
||||
// PathProvider. FLEE differs only in which map is stored in the provider -
|
||||
// DijkstraProvider over an inverted DijkstraMap descends away from the threat,
|
||||
// which matches the old max-distance-neighbor behavior.
|
||||
static BehaviorOutput executeProviderStep(UIEntity& entity, UIGrid& grid) {
|
||||
auto& behavior = entity.behavior;
|
||||
|
||||
if (!behavior.dijkstra_map) {
|
||||
if (!behavior.path_provider) {
|
||||
return {BehaviorResult::NO_ACTION, {}};
|
||||
}
|
||||
|
||||
// Use Dijkstra map to find the lowest-distance neighbor (moving toward target)
|
||||
int cx = entity.cell_position.x;
|
||||
int cy = entity.cell_position.y;
|
||||
float best_dist = std::numeric_limits<float>::max();
|
||||
sf::Vector2i best_cell = {cx, cy};
|
||||
bool found = false;
|
||||
bool ok = false;
|
||||
sf::Vector2i next = behavior.path_provider->nextStep({cx, cy}, grid, &ok);
|
||||
|
||||
sf::Vector2i dirs[] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0},
|
||||
{-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
|
||||
for (auto& dir : dirs) {
|
||||
int nx = cx + dir.x;
|
||||
int ny = cy + dir.y;
|
||||
if (!isCellWalkable(grid, nx, ny)) continue;
|
||||
|
||||
float dist = behavior.dijkstra_map->getDistance(nx, ny);
|
||||
if (dist >= 0 && dist < best_dist) {
|
||||
best_dist = dist;
|
||||
best_cell = {nx, ny};
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found || (best_cell.x == cx && best_cell.y == cy)) {
|
||||
if (!ok) {
|
||||
return {BehaviorResult::BLOCKED, {cx, cy}};
|
||||
}
|
||||
|
||||
return {BehaviorResult::MOVED, best_cell};
|
||||
}
|
||||
|
||||
static BehaviorOutput executeFlee(UIEntity& entity, UIGrid& grid) {
|
||||
auto& behavior = entity.behavior;
|
||||
|
||||
if (!behavior.dijkstra_map) {
|
||||
return {BehaviorResult::NO_ACTION, {}};
|
||||
}
|
||||
|
||||
// Use Dijkstra map to find the highest-distance neighbor (fleeing from target)
|
||||
int cx = entity.cell_position.x;
|
||||
int cy = entity.cell_position.y;
|
||||
float best_dist = -1.0f;
|
||||
sf::Vector2i best_cell = {cx, cy};
|
||||
bool found = false;
|
||||
|
||||
sf::Vector2i dirs[] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0},
|
||||
{-1, -1}, {1, -1}, {-1, 1}, {1, 1}};
|
||||
|
||||
for (auto& dir : dirs) {
|
||||
int nx = cx + dir.x;
|
||||
int ny = cy + dir.y;
|
||||
if (!isCellWalkable(grid, nx, ny)) continue;
|
||||
|
||||
float dist = behavior.dijkstra_map->getDistance(nx, ny);
|
||||
if (dist >= 0 && dist > best_dist) {
|
||||
best_dist = dist;
|
||||
best_cell = {nx, ny};
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found || (best_cell.x == cx && best_cell.y == cy)) {
|
||||
if (next.x == cx && next.y == cy) {
|
||||
return {BehaviorResult::BLOCKED, {cx, cy}};
|
||||
}
|
||||
|
||||
return {BehaviorResult::MOVED, best_cell};
|
||||
return {BehaviorResult::MOVED, next};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -314,8 +277,8 @@ BehaviorOutput executeBehavior(UIEntity& entity, UIGrid& grid) {
|
|||
case BehaviorType::PATROL: return executePatrol(entity, grid);
|
||||
case BehaviorType::LOOP: return executeLoop(entity, grid);
|
||||
case BehaviorType::SLEEP: return executeSleep(entity, grid);
|
||||
case BehaviorType::SEEK: return executeSeek(entity, grid);
|
||||
case BehaviorType::FLEE: return executeFlee(entity, grid);
|
||||
case BehaviorType::SEEK: return executeProviderStep(entity, grid);
|
||||
case BehaviorType::FLEE: return executeProviderStep(entity, grid);
|
||||
}
|
||||
return {BehaviorResult::NO_ACTION, {}};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
class UIEntity;
|
||||
class UIGrid;
|
||||
class DijkstraMap;
|
||||
class PathProvider;
|
||||
|
||||
// =============================================================================
|
||||
// BehaviorType - matches Python mcrfpy.Behavior enum values
|
||||
|
|
@ -60,19 +61,11 @@ struct EntityBehavior {
|
|||
// Sleep data
|
||||
int sleep_turns_remaining = 0;
|
||||
|
||||
// Dijkstra map (for SEEK/FLEE)
|
||||
std::shared_ptr<DijkstraMap> dijkstra_map;
|
||||
// SEEK/FLEE pathfinding strategy (#315). Nullptr means NO_ACTION.
|
||||
std::unique_ptr<PathProvider> path_provider;
|
||||
|
||||
void reset() {
|
||||
type = BehaviorType::IDLE;
|
||||
waypoints.clear();
|
||||
current_waypoint_index = 0;
|
||||
patrol_direction = 1;
|
||||
current_path.clear();
|
||||
path_step_index = 0;
|
||||
sleep_turns_remaining = 0;
|
||||
dijkstra_map = nullptr;
|
||||
}
|
||||
// Defined in EntityBehavior.cpp to avoid needing the full PathProvider type here.
|
||||
void reset();
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#include "PyMouseButton.h"
|
||||
#include "PyInputState.h"
|
||||
#include "PyPerspective.h"
|
||||
#include "PyHeuristic.h"
|
||||
#include "PyBehavior.h"
|
||||
#include "PyTrigger.h"
|
||||
#include "UIGridView.h"
|
||||
|
|
@ -793,6 +794,12 @@ PyObject* PyInit_mcrfpy()
|
|||
PyErr_Clear();
|
||||
}
|
||||
|
||||
// Add Heuristic enum class for A* heuristic selection (#315)
|
||||
PyObject* heuristic_class = PyHeuristic::create_enum_class(m);
|
||||
if (!heuristic_class) {
|
||||
PyErr_Clear();
|
||||
}
|
||||
|
||||
// Add Alignment enum class for automatic child positioning
|
||||
PyObject* alignment_class = PyAlignment::create_enum_class(m);
|
||||
if (!alignment_class) {
|
||||
|
|
|
|||
65
src/PathProvider.cpp
Normal file
65
src/PathProvider.cpp
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#include "PathProvider.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "UIGridPoint.h"
|
||||
|
||||
static bool cellWalkable(UIGrid& grid, int x, int y) {
|
||||
if (x < 0 || x >= grid.grid_w || y < 0 || y >= grid.grid_h) return false;
|
||||
return grid.at(x, y).walkable;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// DijkstraProvider
|
||||
// -----------------------------------------------------------------------------
|
||||
DijkstraProvider::DijkstraProvider(std::shared_ptr<DijkstraMap> map)
|
||||
: map_(std::move(map)) {}
|
||||
|
||||
sf::Vector2i DijkstraProvider::nextStep(sf::Vector2i from, UIGrid& /*grid*/, bool* ok) {
|
||||
if (!map_) {
|
||||
if (ok) *ok = false;
|
||||
return {-1, -1};
|
||||
}
|
||||
bool valid = false;
|
||||
sf::Vector2i step = map_->descentStep(from.x, from.y, &valid);
|
||||
if (ok) *ok = valid;
|
||||
return step;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// AStarProvider
|
||||
// -----------------------------------------------------------------------------
|
||||
AStarProvider::AStarProvider(std::vector<sf::Vector2i> path)
|
||||
: path_(std::move(path)) {}
|
||||
|
||||
sf::Vector2i AStarProvider::nextStep(sf::Vector2i /*from*/, UIGrid& /*grid*/, bool* ok) {
|
||||
if (index_ >= path_.size()) {
|
||||
if (ok) *ok = false;
|
||||
return {-1, -1};
|
||||
}
|
||||
if (ok) *ok = true;
|
||||
return path_[index_++];
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// TargetProvider
|
||||
// -----------------------------------------------------------------------------
|
||||
TargetProvider::TargetProvider(sf::Vector2i target)
|
||||
: target_(target) {}
|
||||
|
||||
sf::Vector2i TargetProvider::nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) {
|
||||
int dx = target_.x - from.x;
|
||||
int dy = target_.y - from.y;
|
||||
if (dx == 0 && dy == 0) {
|
||||
if (ok) *ok = false;
|
||||
return {-1, -1}; // already at target
|
||||
}
|
||||
int sx = (dx > 0) ? 1 : ((dx < 0) ? -1 : 0);
|
||||
int sy = (dy > 0) ? 1 : ((dy < 0) ? -1 : 0);
|
||||
sf::Vector2i step{from.x + sx, from.y + sy};
|
||||
if (!cellWalkable(grid, step.x, step.y)) {
|
||||
if (ok) *ok = false;
|
||||
return {-1, -1};
|
||||
}
|
||||
if (ok) *ok = true;
|
||||
return step;
|
||||
}
|
||||
64
src/PathProvider.h
Normal file
64
src/PathProvider.h
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class UIGrid;
|
||||
class DijkstraMap;
|
||||
|
||||
// =============================================================================
|
||||
// PathProvider (#315) - strategy interface for "what's my next cell?"
|
||||
//
|
||||
// EntityBehavior's SEEK/FLEE execute step() by asking the active PathProvider
|
||||
// for a single cell. Three concrete providers satisfy every pathfinding shape
|
||||
// the engine exposes today.
|
||||
// =============================================================================
|
||||
class PathProvider {
|
||||
public:
|
||||
virtual ~PathProvider() = default;
|
||||
|
||||
// Return the next cell to step to. Sets *ok=true on a valid step, false
|
||||
// otherwise. The provider is responsible for walkability checks -
|
||||
// DijkstraProvider relies on the TCOD map used at compute time,
|
||||
// TargetProvider re-queries the live grid, AStarProvider trusts the
|
||||
// pre-computed path.
|
||||
virtual sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) = 0;
|
||||
|
||||
// Hint for providers that hold iteration state (currently only A*).
|
||||
virtual void reset() {}
|
||||
};
|
||||
|
||||
// Descend a precomputed DijkstraMap. For SEEK, pass the map as-is; for FLEE,
|
||||
// pass DijkstraMap.inverted().
|
||||
class DijkstraProvider : public PathProvider {
|
||||
public:
|
||||
explicit DijkstraProvider(std::shared_ptr<DijkstraMap> map);
|
||||
sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<DijkstraMap> map_;
|
||||
};
|
||||
|
||||
// Replay a pre-computed A* path.
|
||||
class AStarProvider : public PathProvider {
|
||||
public:
|
||||
explicit AStarProvider(std::vector<sf::Vector2i> path);
|
||||
sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) override;
|
||||
void reset() override { index_ = 0; }
|
||||
|
||||
private:
|
||||
std::vector<sf::Vector2i> path_;
|
||||
size_t index_ = 0;
|
||||
};
|
||||
|
||||
// Take a single Chebyshev step toward a fixed target cell. Used when full
|
||||
// pathfinding is overkill (e.g. an adjacent target). Returns no-step if the
|
||||
// straight-line neighbor is blocked - callers can treat that as BLOCKED.
|
||||
class TargetProvider : public PathProvider {
|
||||
public:
|
||||
explicit TargetProvider(sf::Vector2i target);
|
||||
sf::Vector2i nextStep(sf::Vector2i from, UIGrid& grid, bool* ok) override;
|
||||
|
||||
private:
|
||||
sf::Vector2i target_;
|
||||
};
|
||||
142
src/PyHeuristic.cpp
Normal file
142
src/PyHeuristic.cpp
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#include "PyHeuristic.h"
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
PyObject* PyHeuristic::heuristic_enum_class = nullptr;
|
||||
|
||||
struct HeuristicEntry {
|
||||
const char* name;
|
||||
int value;
|
||||
};
|
||||
|
||||
static const HeuristicEntry heuristic_table[] = {
|
||||
{"EUCLIDEAN", 0},
|
||||
{"MANHATTAN", 1},
|
||||
{"CHEBYSHEV", 2},
|
||||
{"DIAGONAL", 3},
|
||||
{"ZERO", 4},
|
||||
};
|
||||
|
||||
static const int NUM_HEURISTIC_ENTRIES =
|
||||
sizeof(heuristic_table) / sizeof(heuristic_table[0]);
|
||||
|
||||
PyObject* PyHeuristic::create_enum_class(PyObject* module) {
|
||||
std::ostringstream code;
|
||||
code << "from enum import IntEnum\n\n";
|
||||
code << "class Heuristic(IntEnum):\n";
|
||||
code << " \"\"\"Built-in A* heuristic function selector.\n";
|
||||
code << " \n";
|
||||
code << " Values:\n";
|
||||
code << " EUCLIDEAN: sqrt((dx)^2 + (dy)^2). Admissible, default.\n";
|
||||
code << " MANHATTAN: |dx| + |dy|. Admissible on 4-connected grids.\n";
|
||||
code << " CHEBYSHEV: max(|dx|, |dy|). Admissible on 8-connected (diag=1).\n";
|
||||
code << " DIAGONAL: Octile distance. Admissible on 8-connected (diag=sqrt(2)).\n";
|
||||
code << " ZERO: Always returns 0. A* degenerates to Dijkstra.\n";
|
||||
code << " \"\"\"\n";
|
||||
for (int i = 0; i < NUM_HEURISTIC_ENTRIES; i++) {
|
||||
code << " " << heuristic_table[i].name
|
||||
<< " = " << heuristic_table[i].value << "\n";
|
||||
}
|
||||
code << "\n";
|
||||
code << "Heuristic.__hash__ = lambda self: hash(int(self))\n";
|
||||
code << "Heuristic.__repr__ = lambda self: f\"{type(self).__name__}.{self.name}\"\n";
|
||||
code << "Heuristic.__str__ = lambda self: self.name\n";
|
||||
|
||||
std::string code_str = code.str();
|
||||
|
||||
PyObject* globals = PyDict_New();
|
||||
if (!globals) return NULL;
|
||||
PyDict_SetItemString(globals, "__builtins__", PyEval_GetBuiltins());
|
||||
|
||||
PyObject* locals = PyDict_New();
|
||||
if (!locals) { Py_DECREF(globals); return NULL; }
|
||||
|
||||
PyObject* result = PyRun_String(code_str.c_str(), Py_file_input, globals, locals);
|
||||
if (!result) {
|
||||
Py_DECREF(globals);
|
||||
Py_DECREF(locals);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(result);
|
||||
|
||||
PyObject* enum_class = PyDict_GetItemString(locals, "Heuristic");
|
||||
if (!enum_class) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to create Heuristic enum class");
|
||||
Py_DECREF(globals);
|
||||
Py_DECREF(locals);
|
||||
return NULL;
|
||||
}
|
||||
Py_INCREF(enum_class);
|
||||
|
||||
heuristic_enum_class = enum_class;
|
||||
Py_INCREF(heuristic_enum_class);
|
||||
|
||||
if (PyModule_AddObject(module, "Heuristic", enum_class) < 0) {
|
||||
Py_DECREF(enum_class);
|
||||
Py_DECREF(globals);
|
||||
Py_DECREF(locals);
|
||||
heuristic_enum_class = nullptr;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_DECREF(globals);
|
||||
Py_DECREF(locals);
|
||||
return enum_class;
|
||||
}
|
||||
|
||||
int PyHeuristic::from_arg(PyObject* arg, int* out_value) {
|
||||
if (heuristic_enum_class && PyObject_IsInstance(arg, heuristic_enum_class)) {
|
||||
PyObject* value = PyObject_GetAttrString(arg, "value");
|
||||
if (!value) return 0;
|
||||
long val = PyLong_AsLong(value);
|
||||
Py_DECREF(value);
|
||||
if (val == -1 && PyErr_Occurred()) return 0;
|
||||
if (val < 0 || val >= NUM_HEURISTIC_VALUES) {
|
||||
PyErr_Format(PyExc_ValueError, "Invalid Heuristic value: %ld", val);
|
||||
return 0;
|
||||
}
|
||||
*out_value = static_cast<int>(val);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (PyLong_Check(arg)) {
|
||||
long val = PyLong_AsLong(arg);
|
||||
if (val == -1 && PyErr_Occurred()) return 0;
|
||||
if (val < 0 || val >= NUM_HEURISTIC_VALUES) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Invalid Heuristic value: %ld. Must be 0..4.", val);
|
||||
return 0;
|
||||
}
|
||||
*out_value = static_cast<int>(val);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (PyUnicode_Check(arg)) {
|
||||
const char* name = PyUnicode_AsUTF8(arg);
|
||||
if (!name) return 0;
|
||||
for (int i = 0; i < NUM_HEURISTIC_ENTRIES; i++) {
|
||||
if (strcmp(name, heuristic_table[i].name) == 0) {
|
||||
*out_value = heuristic_table[i].value;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Unknown Heuristic: '%s'. Use EUCLIDEAN, MANHATTAN, CHEBYSHEV, DIAGONAL, or ZERO.", name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"Heuristic must be mcrfpy.Heuristic enum member, string, or int");
|
||||
return 0;
|
||||
}
|
||||
|
||||
TCOD_heuristic_func_t PyHeuristic::get_function(int heuristic_value) {
|
||||
switch (heuristic_value) {
|
||||
case EUCLIDEAN: return TCOD_heuristic_euclidean;
|
||||
case MANHATTAN: return TCOD_heuristic_manhattan;
|
||||
case CHEBYSHEV: return TCOD_heuristic_chebyshev;
|
||||
case DIAGONAL: return TCOD_heuristic_diagonal;
|
||||
case ZERO: return TCOD_heuristic_zero;
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
39
src/PyHeuristic.h
Normal file
39
src/PyHeuristic.h
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <libtcod.h>
|
||||
#include <cstdint>
|
||||
|
||||
// Module-level Heuristic enum class (created at runtime using Python's IntEnum)
|
||||
// Stored as a module attribute: mcrfpy.Heuristic
|
||||
//
|
||||
// Values:
|
||||
// EUCLIDEAN = 0 (admissible, default, slowest-optimal)
|
||||
// MANHATTAN = 1 (admissible on 4-connected)
|
||||
// CHEBYSHEV = 2 (admissible on 8-connected, diag cost 1)
|
||||
// DIAGONAL = 3 (octile, admissible on 8-connected, diag cost sqrt(2))
|
||||
// ZERO = 4 (A* degenerates to Dijkstra)
|
||||
class PyHeuristic {
|
||||
public:
|
||||
// Create the Heuristic enum class and add to module.
|
||||
static PyObject* create_enum_class(PyObject* module);
|
||||
|
||||
// Helper to extract a Heuristic value from a Python arg.
|
||||
// Accepts Heuristic enum member, string (enum name), or int 0..4.
|
||||
// Returns 1 on success, 0 on error (with exception set).
|
||||
static int from_arg(PyObject* arg, int* out_value);
|
||||
|
||||
// Returns the libtcod built-in heuristic function pointer for a given value.
|
||||
// Returns nullptr if value is invalid.
|
||||
static TCOD_heuristic_func_t get_function(int heuristic_value);
|
||||
|
||||
// Cached reference to the Heuristic enum class for fast type checking.
|
||||
static PyObject* heuristic_enum_class;
|
||||
|
||||
static const int NUM_HEURISTIC_VALUES = 5;
|
||||
static const int EUCLIDEAN = 0;
|
||||
static const int MANHATTAN = 1;
|
||||
static const int CHEBYSHEV = 2;
|
||||
static const int DIAGONAL = 3;
|
||||
static const int ZERO = 4;
|
||||
};
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
#include "UIGrid.h"
|
||||
#include "UIGridView.h" // #252: Entity.grid accepts GridView
|
||||
#include "UIGridPathfinding.h"
|
||||
#include "PathProvider.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
|
@ -1512,14 +1513,16 @@ int UIEntity::set_sight_radius(PyUIEntityObject* self, PyObject* value, void* cl
|
|||
}
|
||||
|
||||
PyObject* UIEntity::py_set_behavior(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"type", "waypoints", "turns", "path", nullptr};
|
||||
static const char* kwlist[] = {"type", "waypoints", "turns", "path", "pathfinder", nullptr};
|
||||
int type_val = 0;
|
||||
PyObject* waypoints_obj = nullptr;
|
||||
int turns = 0;
|
||||
PyObject* path_obj = nullptr;
|
||||
PyObject* pathfinder_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|OiO", const_cast<char**>(kwlist),
|
||||
&type_val, &waypoints_obj, &turns, &path_obj)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|OiOO", const_cast<char**>(kwlist),
|
||||
&type_val, &waypoints_obj, &turns, &path_obj,
|
||||
&pathfinder_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -1582,6 +1585,35 @@ PyObject* UIEntity::py_set_behavior(PyUIEntityObject* self, PyObject* args, PyOb
|
|||
behavior.sleep_turns_remaining = turns;
|
||||
}
|
||||
|
||||
// Parse pathfinder (#315): DijkstraMap, AStarPath, or (x, y) target tuple.
|
||||
if (pathfinder_obj && pathfinder_obj != Py_None) {
|
||||
if (PyObject_IsInstance(pathfinder_obj, (PyObject*)&mcrfpydef::PyDijkstraMapType)) {
|
||||
auto* dmap = (PyDijkstraMapObject*)pathfinder_obj;
|
||||
if (!dmap->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "pathfinder: DijkstraMap is invalid");
|
||||
return NULL;
|
||||
}
|
||||
behavior.path_provider = std::make_unique<DijkstraProvider>(dmap->data);
|
||||
} else if (PyObject_IsInstance(pathfinder_obj, (PyObject*)&mcrfpydef::PyAStarPathType)) {
|
||||
auto* apath = (PyAStarPathObject*)pathfinder_obj;
|
||||
// Copy remaining steps - the provider owns its own iteration state.
|
||||
std::vector<sf::Vector2i> steps(
|
||||
apath->path.begin() + apath->current_index,
|
||||
apath->path.end());
|
||||
behavior.path_provider = std::make_unique<AStarProvider>(std::move(steps));
|
||||
} else if (PyTuple_Check(pathfinder_obj) && PyTuple_Size(pathfinder_obj) == 2) {
|
||||
long tx = PyLong_AsLong(PyTuple_GetItem(pathfinder_obj, 0));
|
||||
long ty = PyLong_AsLong(PyTuple_GetItem(pathfinder_obj, 1));
|
||||
if (PyErr_Occurred()) return NULL;
|
||||
behavior.path_provider = std::make_unique<TargetProvider>(
|
||||
sf::Vector2i(static_cast<int>(tx), static_cast<int>(ty)));
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"pathfinder must be a DijkstraMap, AStarPath, or (x, y) tuple");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,49 +5,72 @@
|
|||
#include "McRFPy_API.h"
|
||||
#include "PyHeightMap.h"
|
||||
#include "PyPositionHelper.h"
|
||||
#include "PyHeuristic.h"
|
||||
#include "PyDiscreteMap.h"
|
||||
|
||||
//=============================================================================
|
||||
// DijkstraMap Implementation
|
||||
//=============================================================================
|
||||
|
||||
DijkstraMap::DijkstraMap(TCODMap* map, int root_x, int root_y, float diag_cost)
|
||||
: tcod_map(map)
|
||||
: tcod_dijkstra(nullptr)
|
||||
, tcod_map(map)
|
||||
, root(root_x, root_y)
|
||||
, diagonal_cost(diag_cost)
|
||||
, map_width(map ? map->getWidth() : 0)
|
||||
, map_height(map ? map->getHeight() : 0)
|
||||
{
|
||||
tcod_dijkstra = new TCODDijkstra(tcod_map, diagonal_cost);
|
||||
tcod_dijkstra->compute(root_x, root_y); // Compute immediately at creation
|
||||
roots.push_back(sf::Vector2i(root_x, root_y));
|
||||
if (tcod_map) {
|
||||
tcod_dijkstra = TCOD_dijkstra_new(tcod_map->data, diagonal_cost);
|
||||
TCOD_dijkstra_compute(tcod_dijkstra, root_x, root_y);
|
||||
}
|
||||
}
|
||||
|
||||
DijkstraMap::DijkstraMap(TCODMap* map, const std::vector<sf::Vector2i>& roots_in, float diag_cost)
|
||||
: tcod_dijkstra(nullptr)
|
||||
, tcod_map(map)
|
||||
, root(roots_in.empty() ? sf::Vector2i(-1, -1) : roots_in.front())
|
||||
, roots(roots_in)
|
||||
, diagonal_cost(diag_cost)
|
||||
, map_width(map ? map->getWidth() : 0)
|
||||
, map_height(map ? map->getHeight() : 0)
|
||||
{
|
||||
if (!tcod_map || roots.empty()) return;
|
||||
|
||||
tcod_dijkstra = TCOD_dijkstra_new(tcod_map->data, diagonal_cost);
|
||||
|
||||
if (roots.size() == 1) {
|
||||
TCOD_dijkstra_compute(tcod_dijkstra, roots[0].x, roots[0].y);
|
||||
} else {
|
||||
std::vector<int> xs, ys;
|
||||
xs.reserve(roots.size());
|
||||
ys.reserve(roots.size());
|
||||
for (auto& r : roots) { xs.push_back(r.x); ys.push_back(r.y); }
|
||||
TCOD_dijkstra_compute_multi(tcod_dijkstra,
|
||||
static_cast<int>(roots.size()), xs.data(), ys.data());
|
||||
}
|
||||
}
|
||||
|
||||
DijkstraMap::~DijkstraMap() {
|
||||
if (tcod_dijkstra) {
|
||||
delete tcod_dijkstra;
|
||||
TCOD_dijkstra_delete(tcod_dijkstra);
|
||||
tcod_dijkstra = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
float DijkstraMap::getDistance(int x, int y) const {
|
||||
if (!tcod_dijkstra) return -1.0f;
|
||||
return tcod_dijkstra->getDistance(x, y);
|
||||
}
|
||||
|
||||
int DijkstraMap::getWidth() const {
|
||||
return map_width;
|
||||
}
|
||||
|
||||
int DijkstraMap::getHeight() const {
|
||||
return map_height;
|
||||
return TCOD_dijkstra_get_distance(tcod_dijkstra, x, y);
|
||||
}
|
||||
|
||||
std::vector<sf::Vector2i> DijkstraMap::getPathFrom(int x, int y) const {
|
||||
std::vector<sf::Vector2i> path;
|
||||
if (!tcod_dijkstra) return path;
|
||||
|
||||
if (tcod_dijkstra->setPath(x, y)) {
|
||||
if (TCOD_dijkstra_path_set(tcod_dijkstra, x, y)) {
|
||||
int px, py;
|
||||
while (tcod_dijkstra->walk(&px, &py)) {
|
||||
while (TCOD_dijkstra_path_walk(tcod_dijkstra, &px, &py)) {
|
||||
path.push_back(sf::Vector2i(px, py));
|
||||
}
|
||||
}
|
||||
|
|
@ -60,22 +83,47 @@ sf::Vector2i DijkstraMap::stepFrom(int x, int y, bool* valid) const {
|
|||
return sf::Vector2i(-1, -1);
|
||||
}
|
||||
|
||||
if (!tcod_dijkstra->setPath(x, y)) {
|
||||
if (!TCOD_dijkstra_path_set(tcod_dijkstra, x, y)) {
|
||||
if (valid) *valid = false;
|
||||
return sf::Vector2i(-1, -1);
|
||||
}
|
||||
|
||||
int px, py;
|
||||
if (tcod_dijkstra->walk(&px, &py)) {
|
||||
if (TCOD_dijkstra_path_walk(tcod_dijkstra, &px, &py)) {
|
||||
if (valid) *valid = true;
|
||||
return sf::Vector2i(px, py);
|
||||
}
|
||||
|
||||
// At root or no path
|
||||
if (valid) *valid = false;
|
||||
return sf::Vector2i(-1, -1);
|
||||
}
|
||||
|
||||
void DijkstraMap::invertInPlace() {
|
||||
if (tcod_dijkstra) {
|
||||
TCOD_dijkstra_invert(tcod_dijkstra);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<DijkstraMap> DijkstraMap::inverted() const {
|
||||
// Recompute from the stored roots, then invert. This preserves the invariant that
|
||||
// the original's distance field is unchanged.
|
||||
auto copy = std::make_shared<DijkstraMap>(tcod_map, roots, diagonal_cost);
|
||||
copy->invertInPlace();
|
||||
return copy;
|
||||
}
|
||||
|
||||
sf::Vector2i DijkstraMap::descentStep(int x, int y, bool* valid) const {
|
||||
if (!tcod_dijkstra) {
|
||||
if (valid) *valid = false;
|
||||
return sf::Vector2i(-1, -1);
|
||||
}
|
||||
int out_x = -1, out_y = -1;
|
||||
bool ok = TCOD_dijkstra_get_descent(tcod_dijkstra, x, y, &out_x, &out_y);
|
||||
if (valid) *valid = ok;
|
||||
if (!ok) return sf::Vector2i(-1, -1);
|
||||
return sf::Vector2i(out_x, out_y);
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// Helper Functions
|
||||
//=============================================================================
|
||||
|
|
@ -405,6 +453,48 @@ PyObject* UIGridPathfinding::DijkstraMap_get_root(PyDijkstraMapObject* self, voi
|
|||
return PyVector(sf::Vector2f(static_cast<float>(root.x), static_cast<float>(root.y))).pyObject();
|
||||
}
|
||||
|
||||
PyObject* UIGridPathfinding::DijkstraMap_invert(PyDijkstraMapObject* self, PyObject* args) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid");
|
||||
return nullptr;
|
||||
}
|
||||
auto new_map = self->data->inverted();
|
||||
if (!new_map) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "invert() failed");
|
||||
return nullptr;
|
||||
}
|
||||
PyDijkstraMapObject* result = (PyDijkstraMapObject*)mcrfpydef::PyDijkstraMapType.tp_alloc(
|
||||
&mcrfpydef::PyDijkstraMapType, 0);
|
||||
if (!result) return nullptr;
|
||||
new (&result->data) std::shared_ptr<DijkstraMap>(new_map);
|
||||
return (PyObject*)result;
|
||||
}
|
||||
|
||||
PyObject* UIGridPathfinding::DijkstraMap_descent_step(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"pos", nullptr};
|
||||
PyObject* pos_obj = nullptr;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(kwlist), &pos_obj)) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid");
|
||||
return nullptr;
|
||||
}
|
||||
int x, y;
|
||||
if (!ExtractPosition(pos_obj, &x, &y, nullptr, "pos")) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!dijkstra_bounds_check(self->data.get(), x, y)) return nullptr;
|
||||
|
||||
bool valid = false;
|
||||
sf::Vector2i step = self->data->descentStep(x, y, &valid);
|
||||
if (!valid) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
return PyVector(sf::Vector2f(static_cast<float>(step.x),
|
||||
static_cast<float>(step.y))).pyObject();
|
||||
}
|
||||
|
||||
PyObject* UIGridPathfinding::DijkstraMap_to_heightmap(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"size", "unreachable", nullptr};
|
||||
PyObject* size_obj = nullptr;
|
||||
|
|
@ -520,14 +610,18 @@ static void restoreCollisionLabel(GridData* grid,
|
|||
}
|
||||
|
||||
PyObject* UIGridPathfinding::Grid_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"start", "end", "diagonal_cost", "collide", NULL};
|
||||
static const char* kwlist[] = {"start", "end", "diagonal_cost", "collide",
|
||||
"heuristic", "weight", NULL};
|
||||
PyObject* start_obj = NULL;
|
||||
PyObject* end_obj = NULL;
|
||||
float diagonal_cost = 1.41f;
|
||||
const char* collide_label = NULL;
|
||||
PyObject* heuristic_obj = NULL;
|
||||
float heuristic_weight = 1.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|fz", const_cast<char**>(kwlist),
|
||||
&start_obj, &end_obj, &diagonal_cost, &collide_label)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|fzOf", const_cast<char**>(kwlist),
|
||||
&start_obj, &end_obj, &diagonal_cost, &collide_label,
|
||||
&heuristic_obj, &heuristic_weight)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -536,6 +630,11 @@ PyObject* UIGridPathfinding::Grid_find_path(PyUIGridObject* self, PyObject* args
|
|||
return NULL;
|
||||
}
|
||||
|
||||
if (heuristic_weight <= 0.0f) {
|
||||
PyErr_SetString(PyExc_ValueError, "weight must be positive");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int x1, y1, x2, y2;
|
||||
if (!ExtractPosition(start_obj, &x1, &y1, self->data.get(), "start")) {
|
||||
return NULL;
|
||||
|
|
@ -551,51 +650,159 @@ PyObject* UIGridPathfinding::Grid_find_path(PyUIGridObject* self, PyObject* args
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Resolve heuristic selection before any allocations so we fail fast on bad args.
|
||||
TCOD_heuristic_func_t heuristic_func = nullptr;
|
||||
if (heuristic_obj && heuristic_obj != Py_None) {
|
||||
int hval = 0;
|
||||
if (!PyHeuristic::from_arg(heuristic_obj, &hval)) {
|
||||
return NULL;
|
||||
}
|
||||
heuristic_func = PyHeuristic::get_function(hval);
|
||||
}
|
||||
|
||||
// Mark-and-restore: temporarily block cells occupied by entities with collide label
|
||||
std::string label_str = collide_label ? collide_label : "";
|
||||
auto restore_list = markCollisionLabel(self->data.get(), label_str);
|
||||
|
||||
// Compute path using temporary TCODPath
|
||||
TCODPath tcod_path(self->data->getTCODMap(), diagonal_cost);
|
||||
bool found = tcod_path.compute(x1, y1, x2, y2);
|
||||
TCODMap* tcmap = self->data->getTCODMap();
|
||||
|
||||
// Build path handle. Use C API so we can set the heuristic/weight when requested.
|
||||
TCOD_path_t tcod_path = TCOD_path_new_using_map(tcmap->data, diagonal_cost);
|
||||
if (heuristic_func || heuristic_weight != 1.0f) {
|
||||
// Passing null heuristic_func keeps the default (Euclidean) while still allowing
|
||||
// weight override; non-null installs the chosen built-in.
|
||||
TCOD_path_set_heuristic(tcod_path, heuristic_func, heuristic_weight);
|
||||
}
|
||||
|
||||
bool found = TCOD_path_compute(tcod_path, x1, y1, x2, y2);
|
||||
|
||||
// Restore walkability before returning
|
||||
restoreCollisionLabel(self->data.get(), restore_list);
|
||||
|
||||
if (!found) {
|
||||
Py_RETURN_NONE; // No path exists
|
||||
TCOD_path_delete(tcod_path);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Create AStarPath result object
|
||||
PyAStarPathObject* result = (PyAStarPathObject*)mcrfpydef::PyAStarPathType.tp_alloc(
|
||||
&mcrfpydef::PyAStarPathType, 0);
|
||||
if (!result) return NULL;
|
||||
if (!result) {
|
||||
TCOD_path_delete(tcod_path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Initialize
|
||||
new (&result->path) std::vector<sf::Vector2i>();
|
||||
result->current_index = 0;
|
||||
result->origin = sf::Vector2i(x1, y1);
|
||||
result->destination = sf::Vector2i(x2, y2);
|
||||
|
||||
// Copy path data
|
||||
result->path.reserve(tcod_path.size());
|
||||
for (int i = 0; i < tcod_path.size(); i++) {
|
||||
int size = TCOD_path_size(tcod_path);
|
||||
result->path.reserve(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
int px, py;
|
||||
tcod_path.get(i, &px, &py);
|
||||
TCOD_path_get(tcod_path, i, &px, &py);
|
||||
result->path.push_back(sf::Vector2i(px, py));
|
||||
}
|
||||
|
||||
TCOD_path_delete(tcod_path);
|
||||
return (PyObject*)result;
|
||||
}
|
||||
|
||||
// Collect roots from a Python object, which may be:
|
||||
// - a single (x,y) (tuple/list/Vector/Entity)
|
||||
// - a list/iterable of (x,y) positions
|
||||
// - a DiscreteMap mask (non-zero cells become roots)
|
||||
// Returns true on success; populates `out_roots` and `out_mask_used`.
|
||||
// If a DiscreteMap mask is used, caller should prefer the masked C path.
|
||||
static bool collectRoots(PyObject* root_obj, UIGrid* grid,
|
||||
std::vector<sf::Vector2i>* out_roots,
|
||||
PyDiscreteMapObject** out_mask)
|
||||
{
|
||||
out_roots->clear();
|
||||
if (out_mask) *out_mask = nullptr;
|
||||
|
||||
// DiscreteMap mask path
|
||||
if (PyObject_IsInstance(root_obj, (PyObject*)&mcrfpydef::PyDiscreteMapType)) {
|
||||
auto* dmap = (PyDiscreteMapObject*)root_obj;
|
||||
if (!dmap->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "DiscreteMap is invalid");
|
||||
return false;
|
||||
}
|
||||
if (dmap->data->width() != grid->grid_w || dmap->data->height() != grid->grid_h) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"DiscreteMap size (%dx%d) does not match grid size (%dx%d)",
|
||||
dmap->data->width(), dmap->data->height(),
|
||||
grid->grid_w, grid->grid_h);
|
||||
return false;
|
||||
}
|
||||
if (out_mask) *out_mask = dmap;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Single position path (Vector / Entity / (x,y) tuple): ExtractPosition accepts these.
|
||||
int x = 0, y = 0;
|
||||
if (UIGridPathfinding::ExtractPosition(root_obj, &x, &y, grid, "root")) {
|
||||
if (x < 0 || x >= grid->grid_w || y < 0 || y >= grid->grid_h) {
|
||||
PyErr_SetString(PyExc_ValueError, "Root position out of grid bounds");
|
||||
return false;
|
||||
}
|
||||
out_roots->push_back(sf::Vector2i(x, y));
|
||||
return true;
|
||||
}
|
||||
// ExtractPosition set an error - clear it only if we still have an iterable to try.
|
||||
if (!PyErr_ExceptionMatches(PyExc_TypeError)) {
|
||||
return false;
|
||||
}
|
||||
PyErr_Clear();
|
||||
|
||||
// List/iterable of positions
|
||||
if (PySequence_Check(root_obj) || PyIter_Check(root_obj)) {
|
||||
PyObject* iter = PyObject_GetIter(root_obj);
|
||||
if (!iter) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"roots must be (x,y), a sequence of (x,y), or a DiscreteMap mask");
|
||||
return false;
|
||||
}
|
||||
PyObject* item;
|
||||
while ((item = PyIter_Next(iter)) != NULL) {
|
||||
int rx = 0, ry = 0;
|
||||
if (!UIGridPathfinding::ExtractPosition(item, &rx, &ry, grid, "root")) {
|
||||
Py_DECREF(item);
|
||||
Py_DECREF(iter);
|
||||
return false;
|
||||
}
|
||||
Py_DECREF(item);
|
||||
if (rx < 0 || rx >= grid->grid_w || ry < 0 || ry >= grid->grid_h) {
|
||||
Py_DECREF(iter);
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Root (%d,%d) out of grid bounds", rx, ry);
|
||||
return false;
|
||||
}
|
||||
out_roots->push_back(sf::Vector2i(rx, ry));
|
||||
}
|
||||
Py_DECREF(iter);
|
||||
if (PyErr_Occurred()) return false;
|
||||
if (out_roots->empty()) {
|
||||
PyErr_SetString(PyExc_ValueError, "roots sequence is empty");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"roots must be (x,y), a sequence of (x,y), or a DiscreteMap mask");
|
||||
return false;
|
||||
}
|
||||
|
||||
PyObject* UIGridPathfinding::Grid_get_dijkstra_map(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"root", "diagonal_cost", "collide", NULL};
|
||||
static const char* kwlist[] = {"root", "diagonal_cost", "collide", "roots", NULL};
|
||||
PyObject* root_obj = NULL;
|
||||
PyObject* roots_obj = NULL;
|
||||
float diagonal_cost = 1.41f;
|
||||
const char* collide_label = NULL;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|fz", const_cast<char**>(kwlist),
|
||||
&root_obj, &diagonal_cost, &collide_label)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OfzO", const_cast<char**>(kwlist),
|
||||
&root_obj, &diagonal_cost, &collide_label, &roots_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
|
@ -604,50 +811,82 @@ PyObject* UIGridPathfinding::Grid_get_dijkstra_map(PyUIGridObject* self, PyObjec
|
|||
return NULL;
|
||||
}
|
||||
|
||||
int root_x, root_y;
|
||||
if (!ExtractPosition(root_obj, &root_x, &root_y, self->data.get(), "root")) {
|
||||
// Accept either `root=` (back-compat, also accepts multi-input now) or `roots=`.
|
||||
PyObject* input_obj = roots_obj ? roots_obj : root_obj;
|
||||
if (!input_obj) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"get_dijkstra_map() requires 'root' or 'roots' argument");
|
||||
return NULL;
|
||||
}
|
||||
if (roots_obj && root_obj) {
|
||||
PyErr_SetString(PyExc_TypeError,
|
||||
"get_dijkstra_map(): pass 'root' or 'roots', not both");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Bounds check
|
||||
if (root_x < 0 || root_x >= self->data->grid_w || root_y < 0 || root_y >= self->data->grid_h) {
|
||||
PyErr_SetString(PyExc_ValueError, "Root position out of grid bounds");
|
||||
std::vector<sf::Vector2i> roots;
|
||||
PyDiscreteMapObject* mask_obj = nullptr;
|
||||
if (!collectRoots(input_obj, self->data.get(), &roots, &mask_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
std::string label_str = collide_label ? collide_label : "";
|
||||
auto key = std::make_tuple(root_x, root_y, label_str);
|
||||
|
||||
// Check cache
|
||||
auto it = self->data->dijkstra_maps.find(key);
|
||||
if (it != self->data->dijkstra_maps.end()) {
|
||||
// Check diagonal cost matches
|
||||
if (std::abs(it->second->getDiagonalCost() - diagonal_cost) < 0.001f) {
|
||||
// Return existing
|
||||
PyDijkstraMapObject* result = (PyDijkstraMapObject*)mcrfpydef::PyDijkstraMapType.tp_alloc(
|
||||
&mcrfpydef::PyDijkstraMapType, 0);
|
||||
if (!result) return NULL;
|
||||
new (&result->data) std::shared_ptr<DijkstraMap>(it->second);
|
||||
return (PyObject*)result;
|
||||
// Cache path for the common single-root case (preserves prior behavior).
|
||||
if (!mask_obj && roots.size() == 1) {
|
||||
auto key = std::make_tuple(roots[0].x, roots[0].y, label_str);
|
||||
auto it = self->data->dijkstra_maps.find(key);
|
||||
if (it != self->data->dijkstra_maps.end()) {
|
||||
if (std::abs(it->second->getDiagonalCost() - diagonal_cost) < 0.001f) {
|
||||
PyDijkstraMapObject* result = (PyDijkstraMapObject*)mcrfpydef::PyDijkstraMapType.tp_alloc(
|
||||
&mcrfpydef::PyDijkstraMapType, 0);
|
||||
if (!result) return NULL;
|
||||
new (&result->data) std::shared_ptr<DijkstraMap>(it->second);
|
||||
return (PyObject*)result;
|
||||
}
|
||||
self->data->dijkstra_maps.erase(it);
|
||||
}
|
||||
// Different diagonal cost - remove old one
|
||||
self->data->dijkstra_maps.erase(it);
|
||||
}
|
||||
|
||||
// Mark-and-restore: temporarily block cells with collide label
|
||||
auto restore_list = markCollisionLabel(self->data.get(), label_str);
|
||||
|
||||
// Create new DijkstraMap
|
||||
auto dijkstra = std::make_shared<DijkstraMap>(
|
||||
self->data->getTCODMap(), root_x, root_y, diagonal_cost);
|
||||
std::shared_ptr<DijkstraMap> dijkstra;
|
||||
TCODMap* tcmap = self->data->getTCODMap();
|
||||
|
||||
if (mask_obj) {
|
||||
// Translate mask -> explicit root list, then drive compute_multi. Distance-only
|
||||
// results are identical to compute_masked; this keeps DijkstraMap's invariant
|
||||
// that it always holds exactly one computed TCOD_Dijkstra handle.
|
||||
std::vector<sf::Vector2i> mask_roots;
|
||||
const uint8_t* buf = mask_obj->data->data();
|
||||
int w = mask_obj->data->width();
|
||||
int h = mask_obj->data->height();
|
||||
mask_roots.reserve(static_cast<size_t>(w) * 4); // heuristic
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
if (buf[y * w + x] != 0) {
|
||||
mask_roots.push_back(sf::Vector2i(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (mask_roots.empty()) {
|
||||
restoreCollisionLabel(self->data.get(), restore_list);
|
||||
PyErr_SetString(PyExc_ValueError, "DiscreteMap mask has no non-zero cells");
|
||||
return NULL;
|
||||
}
|
||||
dijkstra = std::make_shared<DijkstraMap>(tcmap, mask_roots, diagonal_cost);
|
||||
} else {
|
||||
dijkstra = std::make_shared<DijkstraMap>(tcmap, roots, diagonal_cost);
|
||||
}
|
||||
|
||||
// Restore walkability
|
||||
restoreCollisionLabel(self->data.get(), restore_list);
|
||||
|
||||
// Cache it
|
||||
self->data->dijkstra_maps[key] = dijkstra;
|
||||
// Cache only single-root case
|
||||
if (!mask_obj && roots.size() == 1) {
|
||||
auto key = std::make_tuple(roots[0].x, roots[0].y, label_str);
|
||||
self->data->dijkstra_maps[key] = dijkstra;
|
||||
}
|
||||
|
||||
// Return Python wrapper
|
||||
PyDijkstraMapObject* result = (PyDijkstraMapObject*)mcrfpydef::PyDijkstraMapType.tp_alloc(
|
||||
&mcrfpydef::PyDijkstraMapType, 0);
|
||||
if (!result) return NULL;
|
||||
|
|
@ -800,6 +1039,27 @@ PyMethodDef PyDijkstraMap_methods[] = {
|
|||
"Returns:\n"
|
||||
" HeightMap with distance values as heights."},
|
||||
|
||||
{"invert", (PyCFunction)UIGridPathfinding::DijkstraMap_invert, METH_NOARGS,
|
||||
"invert() -> DijkstraMap\n\n"
|
||||
"Return a NEW DijkstraMap whose distance field is the safety field.\n\n"
|
||||
"Cells near a root become high values and cells far from any root become\n"
|
||||
"low values. Combined with step_from or descent_step, this gives flee\n"
|
||||
"behavior: descend the inverted map to move away from the original roots.\n\n"
|
||||
"The original DijkstraMap is unchanged.\n\n"
|
||||
"Returns:\n"
|
||||
" New DijkstraMap with inverted distances."},
|
||||
|
||||
{"descent_step", (PyCFunction)UIGridPathfinding::DijkstraMap_descent_step, METH_VARARGS | METH_KEYWORDS,
|
||||
"descent_step(pos) -> Vector | None\n\n"
|
||||
"Get the adjacent cell with the lowest distance (steepest descent).\n\n"
|
||||
"Unlike step_from (which follows the path set by path_from), descent_step\n"
|
||||
"always returns the best neighbor in a single hop. Useful for AI that\n"
|
||||
"reacts to the current distance field rather than following a fixed path.\n\n"
|
||||
"Args:\n"
|
||||
" pos: Current position as Vector, Entity, or (x, y) tuple.\n\n"
|
||||
"Returns:\n"
|
||||
" Next position as Vector, or None if pos is a local minimum or off-grid."},
|
||||
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,15 @@ struct PyAStarPathObject {
|
|||
|
||||
class DijkstraMap {
|
||||
public:
|
||||
// Single-root construction (back-compat).
|
||||
DijkstraMap(TCODMap* map, int root_x, int root_y, float diagonal_cost);
|
||||
|
||||
// Multi-root construction (#315). roots must be non-empty.
|
||||
DijkstraMap(TCODMap* map, const std::vector<sf::Vector2i>& roots, float diagonal_cost);
|
||||
|
||||
~DijkstraMap();
|
||||
|
||||
// Non-copyable (owns TCODDijkstra)
|
||||
// Non-copyable (owns TCOD_Dijkstra)
|
||||
DijkstraMap(const DijkstraMap&) = delete;
|
||||
DijkstraMap& operator=(const DijkstraMap&) = delete;
|
||||
|
||||
|
|
@ -40,18 +45,35 @@ public:
|
|||
std::vector<sf::Vector2i> getPathFrom(int x, int y) const;
|
||||
sf::Vector2i stepFrom(int x, int y, bool* valid = nullptr) const;
|
||||
|
||||
// Phase B: FLEE primitives (#315)
|
||||
// invertInPlace() mutates this map's distance field. Prefer inverted() in new code —
|
||||
// the Python surface exposes the non-mutating form to keep maps immutable after
|
||||
// creation.
|
||||
void invertInPlace();
|
||||
// Returns a freshly computed DijkstraMap with the same roots and diagonal_cost,
|
||||
// then inverts its distance field. The caller owns the returned shared_ptr.
|
||||
std::shared_ptr<DijkstraMap> inverted() const;
|
||||
// descent_step returns the next cell along steepest descent, or (-1,-1) + valid=false.
|
||||
sf::Vector2i descentStep(int x, int y, bool* valid = nullptr) const;
|
||||
|
||||
// Accessors
|
||||
sf::Vector2i getRoot() const { return root; }
|
||||
sf::Vector2i getRoot() const { return root; } // First root for multi-root
|
||||
const std::vector<sf::Vector2i>& getRoots() const { return roots; }
|
||||
bool isMultiRoot() const { return roots.size() > 1; }
|
||||
float getDiagonalCost() const { return diagonal_cost; }
|
||||
int getWidth() const;
|
||||
int getHeight() const;
|
||||
int getWidth() const { return map_width; }
|
||||
int getHeight() const { return map_height; }
|
||||
|
||||
// Raw C handle, for internal use in new constructor paths (e.g. from_invert).
|
||||
TCOD_Dijkstra* getHandle() const { return tcod_dijkstra; }
|
||||
|
||||
private:
|
||||
TCODDijkstra* tcod_dijkstra; // Owned by this object
|
||||
TCODMap* tcod_map; // Borrowed from Grid
|
||||
TCOD_Dijkstra* tcod_dijkstra; // Owned
|
||||
TCODMap* tcod_map; // Borrowed from Grid
|
||||
sf::Vector2i root;
|
||||
std::vector<sf::Vector2i> roots;
|
||||
float diagonal_cost;
|
||||
int map_width; // Cached from TCODMap at construction
|
||||
int map_width;
|
||||
int map_height;
|
||||
};
|
||||
|
||||
|
|
@ -110,6 +132,8 @@ namespace UIGridPathfinding {
|
|||
PyObject* DijkstraMap_path_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||
PyObject* DijkstraMap_step_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||
PyObject* DijkstraMap_to_heightmap(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||
PyObject* DijkstraMap_invert(PyDijkstraMapObject* self, PyObject* args);
|
||||
PyObject* DijkstraMap_descent_step(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Properties
|
||||
PyObject* DijkstraMap_get_root(PyDijkstraMapObject* self, void* closure);
|
||||
|
|
|
|||
|
|
@ -4,82 +4,82 @@
|
|||
"grid": "100x100",
|
||||
"kind": "multi_root",
|
||||
"roots": 1,
|
||||
"mean_ms": 0.7709094556048512
|
||||
"mean_ms": 0.6948539987206459
|
||||
},
|
||||
{
|
||||
"grid": "100x100",
|
||||
"kind": "multi_root",
|
||||
"roots": 2,
|
||||
"mean_ms": 0.7632468361407518
|
||||
"mean_ms": 0.8002225775271654
|
||||
},
|
||||
{
|
||||
"grid": "100x100",
|
||||
"kind": "multi_root",
|
||||
"roots": 5,
|
||||
"mean_ms": 1.200081780552864
|
||||
"mean_ms": 1.1821302119642496
|
||||
},
|
||||
{
|
||||
"grid": "100x100",
|
||||
"kind": "multi_root",
|
||||
"roots": 20,
|
||||
"mean_ms": 2.137616788968444
|
||||
"mean_ms": 2.0206935703754425
|
||||
},
|
||||
{
|
||||
"grid": "100x100",
|
||||
"kind": "mask",
|
||||
"roots": 500,
|
||||
"mean_ms": 30.424197972752154
|
||||
"mean_ms": 24.073211615905166
|
||||
},
|
||||
{
|
||||
"grid": "100x100",
|
||||
"kind": "invert",
|
||||
"mean_ms": 1.0323396185413003
|
||||
"mean_ms": 0.7887090090662241
|
||||
},
|
||||
{
|
||||
"grid": "100x100",
|
||||
"kind": "descent_step_per_call",
|
||||
"mean_us": 0.4075700417160988,
|
||||
"mean_us": 0.3262499812990427,
|
||||
"valid_per_trial": 100
|
||||
},
|
||||
{
|
||||
"grid": "500x500",
|
||||
"kind": "multi_root",
|
||||
"roots": 1,
|
||||
"mean_ms": 26.075413217768073
|
||||
"mean_ms": 20.39959062822163
|
||||
},
|
||||
{
|
||||
"grid": "500x500",
|
||||
"kind": "multi_root",
|
||||
"roots": 2,
|
||||
"mean_ms": 25.83242394030094
|
||||
"mean_ms": 22.81180394347757
|
||||
},
|
||||
{
|
||||
"grid": "500x500",
|
||||
"kind": "multi_root",
|
||||
"roots": 5,
|
||||
"mean_ms": 33.73005616012961
|
||||
"mean_ms": 29.194996040314436
|
||||
},
|
||||
{
|
||||
"grid": "500x500",
|
||||
"kind": "multi_root",
|
||||
"roots": 20,
|
||||
"mean_ms": 78.58918677084148
|
||||
"mean_ms": 67.84450197592378
|
||||
},
|
||||
{
|
||||
"grid": "500x500",
|
||||
"kind": "mask",
|
||||
"roots": 12500,
|
||||
"mean_ms": 18658.679087948985
|
||||
"mean_ms": 19711.241053813137
|
||||
},
|
||||
{
|
||||
"grid": "500x500",
|
||||
"kind": "invert",
|
||||
"mean_ms": 25.918347598053515
|
||||
"mean_ms": 27.875673002563417
|
||||
},
|
||||
{
|
||||
"grid": "500x500",
|
||||
"kind": "descent_step_per_call",
|
||||
"mean_us": 0.3717193566262722,
|
||||
"mean_us": 0.3495373670011759,
|
||||
"valid_per_trial": 2500
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
"grid": "100x100",
|
||||
"entities": 100,
|
||||
"rounds": 1000,
|
||||
"total_sec": 0.07824495097156614,
|
||||
"mean_round_ms": 0.07824495097156614,
|
||||
"p95_round_ms": 0.1227830071002245,
|
||||
"per_entity_step_us": 0.7824495097156614
|
||||
"total_sec": 0.06852402002550662,
|
||||
"mean_round_ms": 0.06852402002550662,
|
||||
"p95_round_ms": 0.09302806574851274,
|
||||
"per_entity_step_us": 0.6852402002550662
|
||||
}
|
||||
|
|
@ -4,31 +4,31 @@
|
|||
"views": 1,
|
||||
"frames": 60,
|
||||
"warmup_frames": 5,
|
||||
"total_sec": 4.710599604761228,
|
||||
"mean_frame_ms": 78.50999341268714,
|
||||
"p95_frame_ms": 90.47448402270675,
|
||||
"implied_fps": 12.737231994702995,
|
||||
"per_view_frame_ms": 78.50999341268714
|
||||
"total_sec": 4.731390134897083,
|
||||
"mean_frame_ms": 78.85650224828473,
|
||||
"p95_frame_ms": 86.92639297805727,
|
||||
"implied_fps": 12.681262438593032,
|
||||
"per_view_frame_ms": 78.85650224828473
|
||||
},
|
||||
{
|
||||
"views": 2,
|
||||
"frames": 60,
|
||||
"warmup_frames": 5,
|
||||
"total_sec": 4.6525509969796985,
|
||||
"mean_frame_ms": 77.54251661632831,
|
||||
"p95_frame_ms": 91.92000096663833,
|
||||
"implied_fps": 12.896150958678424,
|
||||
"per_view_frame_ms": 38.77125830816416
|
||||
"total_sec": 4.975782633875497,
|
||||
"mean_frame_ms": 82.92971056459162,
|
||||
"p95_frame_ms": 91.40842803753912,
|
||||
"implied_fps": 12.058404559619538,
|
||||
"per_view_frame_ms": 41.46485528229581
|
||||
},
|
||||
{
|
||||
"views": 4,
|
||||
"frames": 60,
|
||||
"warmup_frames": 5,
|
||||
"total_sec": 4.727940998389386,
|
||||
"mean_frame_ms": 78.7990166398231,
|
||||
"p95_frame_ms": 99.88687501754612,
|
||||
"implied_fps": 12.690513697281654,
|
||||
"per_view_frame_ms": 19.699754159955774
|
||||
"total_sec": 4.861755113233812,
|
||||
"mean_frame_ms": 81.0292518872302,
|
||||
"p95_frame_ms": 90.46144201420248,
|
||||
"implied_fps": 12.341222172354708,
|
||||
"per_view_frame_ms": 20.25731297180755
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
|
|
|
|||
290
tests/demo/screens/pathfinding_demo.py
Normal file
290
tests/demo/screens/pathfinding_demo.py
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
"""pathfinding_demo.py - Visual demo of the #315 pathfinding primitives.
|
||||
|
||||
Three panels side-by-side on one scene:
|
||||
[Panel 1] A* with selectable heuristic. Keys 1-5 cycle EUCLIDEAN, MANHATTAN,
|
||||
CHEBYSHEV, DIAGONAL, ZERO. Q/W bump the weight by 0.25.
|
||||
[Panel 2] Dijkstra flood from a cursor-controlled root. Arrow keys move the
|
||||
cursor; the distance field re-renders as a blue gradient.
|
||||
[Panel 3] Multi-root FLEE: three "guard" entities flee from a shared set of
|
||||
threats, using an inverted multi-root Dijkstra map. Animated one
|
||||
step per frame tick. Press T to drop a new threat on the panel.
|
||||
|
||||
Also exercises: DijkstraMap.invert(), DijkstraMap.descent_step(),
|
||||
mcrfpy.Heuristic, Grid.find_path(heuristic=, weight=), Grid.get_dijkstra_map(
|
||||
roots=...).
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
GRID_W, GRID_H = 20, 20
|
||||
CELL_PX = 14
|
||||
PANEL_W_PX = GRID_W * CELL_PX
|
||||
GAP = 20
|
||||
|
||||
SCREEN_W = 3 * PANEL_W_PX + 4 * GAP
|
||||
SCREEN_H = GRID_H * CELL_PX + 140
|
||||
|
||||
|
||||
scene = mcrfpy.Scene("pathfinding_demo")
|
||||
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(SCREEN_W, SCREEN_H),
|
||||
fill_color=mcrfpy.Color(18, 18, 26))
|
||||
scene.children.append(bg)
|
||||
|
||||
title = mcrfpy.Caption(text="Pathfinding Next-Gen Demo (#315)", pos=(GAP, 10))
|
||||
title.fill_color = mcrfpy.Color(240, 240, 255)
|
||||
scene.children.append(title)
|
||||
|
||||
|
||||
def make_open_grid(x_off):
|
||||
g = mcrfpy.Grid(grid_size=(GRID_W, GRID_H),
|
||||
pos=(x_off, 50),
|
||||
size=(PANEL_W_PX, GRID_H * CELL_PX))
|
||||
for y in range(GRID_H):
|
||||
for x in range(GRID_W):
|
||||
c = g.at(x, y)
|
||||
wall = (x in (0, GRID_W - 1)) or (y in (0, GRID_H - 1))
|
||||
c.walkable = not wall
|
||||
c.transparent = not wall
|
||||
# A small wedge of walls in the middle for visual interest.
|
||||
for y in range(4, 12):
|
||||
g.at(GRID_W // 2, y).walkable = False
|
||||
g.at(GRID_W // 2, y).transparent = False
|
||||
scene.children.append(g)
|
||||
return g
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Panel 1: A* with heuristic switching
|
||||
# =============================================================================
|
||||
g1 = make_open_grid(GAP)
|
||||
astar_layer = mcrfpy.ColorLayer(z_index=1, name="astar_path")
|
||||
g1.add_layer(astar_layer)
|
||||
|
||||
HEURISTICS = [
|
||||
(mcrfpy.Heuristic.EUCLIDEAN, "EUCLIDEAN"),
|
||||
(mcrfpy.Heuristic.MANHATTAN, "MANHATTAN"),
|
||||
(mcrfpy.Heuristic.CHEBYSHEV, "CHEBYSHEV"),
|
||||
(mcrfpy.Heuristic.DIAGONAL, "DIAGONAL"),
|
||||
(mcrfpy.Heuristic.ZERO, "ZERO"),
|
||||
]
|
||||
state_astar = {"hidx": 0, "weight": 1.0,
|
||||
"start": (2, 10), "end": (GRID_W - 3, 10)}
|
||||
|
||||
cap_astar = mcrfpy.Caption(text="A* heuristic: EUCLIDEAN weight=1.00",
|
||||
pos=(GAP, 50 + GRID_H * CELL_PX + 6))
|
||||
cap_astar.fill_color = mcrfpy.Color(180, 220, 255)
|
||||
scene.children.append(cap_astar)
|
||||
|
||||
|
||||
def redraw_astar():
|
||||
h, hname = HEURISTICS[state_astar["hidx"]]
|
||||
astar_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||||
p = g1.find_path(state_astar["start"], state_astar["end"],
|
||||
heuristic=h, weight=state_astar["weight"])
|
||||
n_steps = 0
|
||||
if p is not None:
|
||||
for step in p:
|
||||
astar_layer.set((int(step.x), int(step.y)),
|
||||
mcrfpy.Color(255, 220, 80, 220))
|
||||
n_steps += 1
|
||||
# Start/end markers.
|
||||
astar_layer.set(state_astar["start"], mcrfpy.Color(80, 255, 120, 255))
|
||||
astar_layer.set(state_astar["end"], mcrfpy.Color(255, 90, 90, 255))
|
||||
cap_astar.text = (f"A* heuristic: {hname} "
|
||||
f"weight={state_astar['weight']:.2f} steps={n_steps}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Panel 2: Dijkstra flood
|
||||
# =============================================================================
|
||||
g2 = make_open_grid(GAP * 2 + PANEL_W_PX)
|
||||
dij_layer = mcrfpy.ColorLayer(z_index=1, name="dij_flood")
|
||||
g2.add_layer(dij_layer)
|
||||
|
||||
state_dij = {"root": (GRID_W // 2 - 3, GRID_H // 2)}
|
||||
|
||||
cap_dij = mcrfpy.Caption(text="Dijkstra flood (arrows move root)",
|
||||
pos=(GAP * 2 + PANEL_W_PX, 50 + GRID_H * CELL_PX + 6))
|
||||
cap_dij.fill_color = mcrfpy.Color(180, 255, 220)
|
||||
scene.children.append(cap_dij)
|
||||
|
||||
|
||||
def redraw_dijkstra():
|
||||
dij_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||||
root = state_dij["root"]
|
||||
if not g2.at(root[0], root[1]).walkable:
|
||||
cap_dij.text = "Dijkstra root on wall - move with arrows"
|
||||
return
|
||||
dmap = g2.get_dijkstra_map(root)
|
||||
# Sample distances to find the maximum for normalization.
|
||||
max_dist = 0.0
|
||||
for y in range(1, GRID_H - 1):
|
||||
for x in range(1, GRID_W - 1):
|
||||
d = dmap.distance((x, y))
|
||||
if d is not None and d > max_dist:
|
||||
max_dist = d
|
||||
if max_dist <= 0:
|
||||
return
|
||||
for y in range(1, GRID_H - 1):
|
||||
for x in range(1, GRID_W - 1):
|
||||
d = dmap.distance((x, y))
|
||||
if d is None:
|
||||
continue
|
||||
t = min(1.0, d / max_dist)
|
||||
# Cool gradient: near-root = bright, far = dark.
|
||||
r = int(40 * t + 80 * (1 - t))
|
||||
gc = int(160 * (1 - t) + 40 * t)
|
||||
bc = int(240 * (1 - t) + 80 * t)
|
||||
dij_layer.set((x, y), mcrfpy.Color(r, gc, bc, 180))
|
||||
dij_layer.set(root, mcrfpy.Color(255, 255, 90, 255))
|
||||
cap_dij.text = f"Dijkstra flood max={max_dist:.1f} root={root}"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Panel 3: Multi-root FLEE
|
||||
# =============================================================================
|
||||
g3 = make_open_grid(GAP * 3 + 2 * PANEL_W_PX)
|
||||
flee_layer = mcrfpy.ColorLayer(z_index=1, name="flee_layer")
|
||||
g3.add_layer(flee_layer)
|
||||
|
||||
state_flee = {
|
||||
"threats": [(3, 3), (GRID_W - 4, GRID_H - 4)],
|
||||
"guards": [(10, 6), (10, 10), (10, 14)],
|
||||
"safety": None,
|
||||
"threat_map": None,
|
||||
}
|
||||
|
||||
cap_flee = mcrfpy.Caption(text="Multi-root FLEE (T adds threat, R resets)",
|
||||
pos=(GAP * 3 + 2 * PANEL_W_PX,
|
||||
50 + GRID_H * CELL_PX + 6))
|
||||
cap_flee.fill_color = mcrfpy.Color(255, 190, 190)
|
||||
scene.children.append(cap_flee)
|
||||
|
||||
|
||||
def recompute_flee():
|
||||
state_flee["threat_map"] = g3.get_dijkstra_map(roots=state_flee["threats"])
|
||||
state_flee["safety"] = state_flee["threat_map"].invert()
|
||||
|
||||
|
||||
def redraw_flee():
|
||||
flee_layer.fill(mcrfpy.Color(0, 0, 0, 0))
|
||||
# Threats: red.
|
||||
for t in state_flee["threats"]:
|
||||
flee_layer.set(t, mcrfpy.Color(255, 60, 60, 255))
|
||||
# Guards: green.
|
||||
for gd in state_flee["guards"]:
|
||||
flee_layer.set(gd, mcrfpy.Color(80, 255, 120, 255))
|
||||
cap_flee.text = (f"FLEE: {len(state_flee['threats'])} threats "
|
||||
f"{len(state_flee['guards'])} guards")
|
||||
|
||||
|
||||
def step_guards():
|
||||
if state_flee["safety"] is None:
|
||||
return
|
||||
new_guards = []
|
||||
for gd in state_flee["guards"]:
|
||||
nxt = state_flee["safety"].descent_step(gd)
|
||||
if nxt is None:
|
||||
new_guards.append(gd)
|
||||
else:
|
||||
candidate = (int(nxt.x), int(nxt.y))
|
||||
# Avoid landing on another guard or a threat.
|
||||
if candidate in new_guards or candidate in state_flee["threats"]:
|
||||
new_guards.append(gd)
|
||||
else:
|
||||
new_guards.append(candidate)
|
||||
state_flee["guards"] = new_guards
|
||||
redraw_flee()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Key handling
|
||||
# =============================================================================
|
||||
instructions = [
|
||||
"Panel 1 (A*): [1-5] heuristic [Q/W] weight -/+",
|
||||
"Panel 2 (Dij): [Arrow keys] move root",
|
||||
"Panel 3 (FLEE): [T] add threat [R] reset (guards auto-step)",
|
||||
"[ESC] quit",
|
||||
]
|
||||
for i, text in enumerate(instructions):
|
||||
c = mcrfpy.Caption(text=text, pos=(GAP, SCREEN_H - 90 + i * 22))
|
||||
c.fill_color = mcrfpy.Color(180, 180, 200)
|
||||
scene.children.append(c)
|
||||
|
||||
|
||||
def on_key(key, state):
|
||||
if state != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
|
||||
# Heuristic switching
|
||||
for i, digit in enumerate([mcrfpy.Key.Num1, mcrfpy.Key.Num2, mcrfpy.Key.Num3,
|
||||
mcrfpy.Key.Num4, mcrfpy.Key.Num5]):
|
||||
if key == digit:
|
||||
state_astar["hidx"] = i
|
||||
redraw_astar()
|
||||
return
|
||||
|
||||
if key == mcrfpy.Key.Q:
|
||||
state_astar["weight"] = max(0.25, state_astar["weight"] - 0.25)
|
||||
redraw_astar()
|
||||
return
|
||||
if key == mcrfpy.Key.W:
|
||||
state_astar["weight"] = min(5.0, state_astar["weight"] + 0.25)
|
||||
redraw_astar()
|
||||
return
|
||||
|
||||
# Dijkstra root movement
|
||||
rx, ry = state_dij["root"]
|
||||
moved = False
|
||||
if key == mcrfpy.Key.LEFT:
|
||||
rx = max(1, rx - 1); moved = True
|
||||
elif key == mcrfpy.Key.RIGHT:
|
||||
rx = min(GRID_W - 2, rx + 1); moved = True
|
||||
elif key == mcrfpy.Key.UP:
|
||||
ry = max(1, ry - 1); moved = True
|
||||
elif key == mcrfpy.Key.DOWN:
|
||||
ry = min(GRID_H - 2, ry + 1); moved = True
|
||||
if moved:
|
||||
state_dij["root"] = (rx, ry)
|
||||
redraw_dijkstra()
|
||||
|
||||
# FLEE panel: drop threat at a random walkable cell.
|
||||
if key == mcrfpy.Key.T:
|
||||
import random
|
||||
for _ in range(20):
|
||||
p = (random.randint(1, GRID_W - 2), random.randint(1, GRID_H - 2))
|
||||
if g3.at(p[0], p[1]).walkable and p not in state_flee["threats"]:
|
||||
state_flee["threats"].append(p)
|
||||
break
|
||||
recompute_flee()
|
||||
redraw_flee()
|
||||
|
||||
if key == mcrfpy.Key.R:
|
||||
state_flee["threats"] = [(3, 3), (GRID_W - 4, GRID_H - 4)]
|
||||
state_flee["guards"] = [(10, 6), (10, 10), (10, 14)]
|
||||
recompute_flee()
|
||||
redraw_flee()
|
||||
|
||||
if key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
|
||||
|
||||
scene.on_key = on_key
|
||||
|
||||
# Step FLEE guards on a timer (6 ticks/sec is slow enough to watch).
|
||||
def tick(timer, runtime):
|
||||
step_guards()
|
||||
|
||||
|
||||
# Initial render
|
||||
redraw_astar()
|
||||
redraw_dijkstra()
|
||||
recompute_flee()
|
||||
redraw_flee()
|
||||
|
||||
mcrfpy.Timer("flee_tick", tick, 160)
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
print("pathfinding_demo loaded - see on-screen instructions.")
|
||||
150
tests/regression/issue_315_path_provider_test.py
Normal file
150
tests/regression/issue_315_path_provider_test.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"""Phase C regression: PathProvider abstraction drives SEEK/FLEE via set_behavior.
|
||||
|
||||
Verifies that each of the three provider shapes (DijkstraMap, AStarPath, and
|
||||
plain target tuple) produces a valid SEEK step, and that swapping pathfinder
|
||||
mid-run changes the next step.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
def make_open_grid(w=20, h=20):
|
||||
scene = mcrfpy.Scene("issue315")
|
||||
mcrfpy.current_scene = scene
|
||||
grid = mcrfpy.Grid(grid_size=(w, h))
|
||||
scene.children.append(grid)
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
c = grid.at(x, y)
|
||||
# Walkable interior, walls on the border.
|
||||
if x == 0 or y == 0 or x == w - 1 or y == h - 1:
|
||||
c.walkable = False
|
||||
c.transparent = False
|
||||
else:
|
||||
c.walkable = True
|
||||
c.transparent = True
|
||||
return grid
|
||||
|
||||
|
||||
def closer_to(p, goal, start):
|
||||
"""True when p is closer than start to goal (Chebyshev)."""
|
||||
def d(a, b): return max(abs(a[0] - b[0]), abs(a[1] - b[1]))
|
||||
return d(p, goal) < d(start, goal)
|
||||
|
||||
|
||||
def test_dijkstra_provider():
|
||||
grid = make_open_grid()
|
||||
e = mcrfpy.Entity((5, 5), grid=grid)
|
||||
e.move_speed = 0
|
||||
goal = (15, 15)
|
||||
dmap = grid.get_dijkstra_map(goal)
|
||||
e.set_behavior(int(mcrfpy.Behavior.SEEK), pathfinder=dmap)
|
||||
|
||||
start = (e.cell_x, e.cell_y)
|
||||
grid.step()
|
||||
ended = (e.cell_x, e.cell_y)
|
||||
assert ended != start, "DijkstraProvider SEEK must move the entity"
|
||||
assert closer_to(ended, goal, start), f"moved away from goal: {start} -> {ended}"
|
||||
print(" DijkstraProvider SEEK: OK")
|
||||
|
||||
|
||||
def test_astar_provider():
|
||||
grid = make_open_grid()
|
||||
e = mcrfpy.Entity((5, 5), grid=grid)
|
||||
e.move_speed = 0
|
||||
goal = (10, 10)
|
||||
path = grid.find_path((5, 5), goal)
|
||||
assert path is not None
|
||||
e.set_behavior(int(mcrfpy.Behavior.SEEK), pathfinder=path)
|
||||
|
||||
start = (e.cell_x, e.cell_y)
|
||||
grid.step()
|
||||
ended = (e.cell_x, e.cell_y)
|
||||
assert ended != start, "AStarProvider SEEK must move the entity"
|
||||
assert closer_to(ended, goal, start), f"moved away from goal: {start} -> {ended}"
|
||||
print(" AStarProvider SEEK: OK")
|
||||
|
||||
|
||||
def test_target_provider():
|
||||
grid = make_open_grid()
|
||||
e = mcrfpy.Entity((5, 5), grid=grid)
|
||||
e.move_speed = 0
|
||||
goal = (10, 10)
|
||||
e.set_behavior(int(mcrfpy.Behavior.SEEK), pathfinder=goal)
|
||||
|
||||
start = (e.cell_x, e.cell_y)
|
||||
grid.step()
|
||||
ended = (e.cell_x, e.cell_y)
|
||||
assert ended != start, "TargetProvider SEEK must move the entity"
|
||||
assert closer_to(ended, goal, start), f"moved away from target: {start} -> {ended}"
|
||||
print(" TargetProvider SEEK: OK")
|
||||
|
||||
|
||||
def test_flee_via_inverted_dijkstra():
|
||||
grid = make_open_grid()
|
||||
e = mcrfpy.Entity((10, 10), grid=grid)
|
||||
e.move_speed = 0
|
||||
threat = (12, 10)
|
||||
threat_map = grid.get_dijkstra_map(threat)
|
||||
safety_map = threat_map.invert()
|
||||
e.set_behavior(int(mcrfpy.Behavior.FLEE), pathfinder=safety_map)
|
||||
|
||||
start = (e.cell_x, e.cell_y)
|
||||
start_dist = threat_map.distance(start)
|
||||
grid.step()
|
||||
ended = (e.cell_x, e.cell_y)
|
||||
assert ended != start, "FLEE must move the entity"
|
||||
new_dist = threat_map.distance(ended)
|
||||
assert new_dist >= start_dist, (
|
||||
f"FLEE should not get closer: d={start_dist} -> {new_dist}")
|
||||
print(" FLEE via inverted DijkstraMap: OK")
|
||||
|
||||
|
||||
def test_provider_swap_midrun():
|
||||
"""Swapping pathfinder mid-run changes the next step."""
|
||||
grid = make_open_grid()
|
||||
e = mcrfpy.Entity((10, 10), grid=grid)
|
||||
e.move_speed = 0
|
||||
|
||||
# First: seek (5, 10) - should move left.
|
||||
e.set_behavior(int(mcrfpy.Behavior.SEEK),
|
||||
pathfinder=grid.get_dijkstra_map((5, 10)))
|
||||
grid.step()
|
||||
after_first = (e.cell_x, e.cell_y)
|
||||
assert after_first[0] < 10, f"expected leftward step, got {after_first}"
|
||||
|
||||
# Swap pathfinder: now seek (15, 10) - should move right from here.
|
||||
e.set_behavior(int(mcrfpy.Behavior.SEEK),
|
||||
pathfinder=grid.get_dijkstra_map((15, 10)))
|
||||
grid.step()
|
||||
after_second = (e.cell_x, e.cell_y)
|
||||
assert after_second[0] > after_first[0], (
|
||||
f"expected rightward step after swap, got {after_first} -> {after_second}")
|
||||
print(" Mid-run provider swap: OK")
|
||||
|
||||
|
||||
def test_invalid_pathfinder_raises():
|
||||
grid = make_open_grid()
|
||||
e = mcrfpy.Entity((5, 5), grid=grid)
|
||||
try:
|
||||
e.set_behavior(int(mcrfpy.Behavior.SEEK), pathfinder="not a pathfinder")
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected TypeError for bad pathfinder argument")
|
||||
print(" Invalid pathfinder rejected: OK")
|
||||
|
||||
|
||||
def main():
|
||||
test_dijkstra_provider()
|
||||
test_astar_provider()
|
||||
test_target_provider()
|
||||
test_flee_via_inverted_dijkstra()
|
||||
test_provider_swap_midrun()
|
||||
test_invalid_pathfinder_raises()
|
||||
print("PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(0)
|
||||
85
tests/unit/dijkstra_flee_test.py
Normal file
85
tests/unit/dijkstra_flee_test.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""DijkstraMap.invert() + descent_step() produce FLEE behavior.
|
||||
|
||||
Build a Dijkstra map rooted on a threat, invert it, and confirm that walking
|
||||
descent steps from a nearby cell strictly increases distance from the threat.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
def make_grid(w, h):
|
||||
g = mcrfpy.Grid(grid_size=(w, h))
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
c = g.at(x, y)
|
||||
c.walkable = True
|
||||
c.transparent = True
|
||||
return g
|
||||
|
||||
|
||||
def main():
|
||||
g = make_grid(20, 20)
|
||||
threat = (10, 10)
|
||||
|
||||
threat_map = g.get_dijkstra_map(threat)
|
||||
assert threat_map.distance(threat) == 0.0
|
||||
|
||||
# invert() returns a NEW map; the original is unchanged.
|
||||
safety_map = threat_map.invert()
|
||||
assert safety_map is not threat_map, "invert() should return a new object"
|
||||
assert threat_map.distance(threat) == 0.0, "original map must not be mutated"
|
||||
# After invert, the threat cell itself is a local minimum (low safety),
|
||||
# and cells far from the threat are peaks.
|
||||
|
||||
# descent_step on the safety map from a cell near the threat must move AWAY,
|
||||
# i.e. its distance in the *original* (threat-rooted) map strictly increases.
|
||||
start = (11, 10)
|
||||
start_dist = threat_map.distance(start)
|
||||
|
||||
pos = start
|
||||
for _ in range(5):
|
||||
nxt = safety_map.descent_step(pos)
|
||||
if nxt is None:
|
||||
break
|
||||
nxt_tuple = (int(nxt.x), int(nxt.y))
|
||||
# Must actually move.
|
||||
assert nxt_tuple != pos, f"descent stuck at {pos}"
|
||||
# Must move to a walkable cell inside the grid.
|
||||
assert 0 <= nxt_tuple[0] < 20 and 0 <= nxt_tuple[1] < 20
|
||||
new_dist = threat_map.distance(nxt_tuple)
|
||||
assert new_dist >= start_dist, (
|
||||
f"FLEE descent from {pos} to {nxt_tuple}: threat distance dropped "
|
||||
f"from {start_dist} to {new_dist}")
|
||||
pos = nxt_tuple
|
||||
start_dist = new_dist
|
||||
|
||||
# descent_step on the original (non-inverted) map from a far cell SEEKs the threat.
|
||||
far = (0, 0)
|
||||
nxt = threat_map.descent_step(far)
|
||||
assert nxt is not None
|
||||
nxt_tuple = (int(nxt.x), int(nxt.y))
|
||||
# Closer to the threat than `far`.
|
||||
assert threat_map.distance(nxt_tuple) < threat_map.distance(far), \
|
||||
"descent on threat_map should SEEK the root"
|
||||
|
||||
# descent_step at the root itself has no better neighbor — returns None.
|
||||
at_root = safety_map.descent_step(threat)
|
||||
# Note: at_root might not be None on the inverted map since the threat is a local
|
||||
# minimum of the inverted field — any neighbor has lower (or equal) value. So allow
|
||||
# either None or a valid step. Just ensure we don't crash.
|
||||
_ = at_root
|
||||
|
||||
# Out-of-bounds raises IndexError.
|
||||
try:
|
||||
safety_map.descent_step((999, 999))
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected IndexError for out-of-bounds descent_step")
|
||||
|
||||
print("PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(0)
|
||||
61
tests/unit/find_path_heuristic_test.py
Normal file
61
tests/unit/find_path_heuristic_test.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""Grid.find_path heuristic/weight kwargs produce valid paths across each built-in."""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
def make_open_grid(w, h):
|
||||
g = mcrfpy.Grid(grid_size=(w, h))
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
c = g.at(x, y)
|
||||
c.walkable = True
|
||||
c.transparent = True
|
||||
return g
|
||||
|
||||
|
||||
def main():
|
||||
g = make_open_grid(30, 30)
|
||||
|
||||
# On an obstacle-free grid every admissible heuristic yields an optimal-length path.
|
||||
# libtcod returns steps (excluding origin), so a diagonal-permitting move from (0,0)
|
||||
# to (20,20) is 20 steps.
|
||||
for h in (mcrfpy.Heuristic.EUCLIDEAN,
|
||||
mcrfpy.Heuristic.MANHATTAN,
|
||||
mcrfpy.Heuristic.CHEBYSHEV,
|
||||
mcrfpy.Heuristic.DIAGONAL,
|
||||
mcrfpy.Heuristic.ZERO):
|
||||
p = g.find_path((0, 0), (20, 20), heuristic=h)
|
||||
assert p is not None, f"no path for {h}"
|
||||
steps = list(p)
|
||||
assert len(steps) == 20, f"heuristic {h} gave {len(steps)} steps, expected 20"
|
||||
# Last step must be the goal.
|
||||
assert (int(steps[-1].x), int(steps[-1].y)) == (20, 20), \
|
||||
f"heuristic {h} did not end at goal"
|
||||
|
||||
# Weighted A* with weight>=1 must still find a path (not necessarily optimal).
|
||||
for w in (1.0, 1.5, 2.0):
|
||||
p = g.find_path((0, 0), (20, 20), heuristic=mcrfpy.Heuristic.EUCLIDEAN, weight=w)
|
||||
assert p is not None, f"no path for weight={w}"
|
||||
steps = list(p)
|
||||
assert len(steps) >= 20, f"weight={w} gave impossibly short path"
|
||||
|
||||
# With an obstacle, the path still reaches the goal.
|
||||
g2 = make_open_grid(30, 30)
|
||||
for y in range(5, 25):
|
||||
g2.at(15, y).walkable = False
|
||||
|
||||
p = g2.find_path((0, 0), (29, 0), heuristic=mcrfpy.Heuristic.MANHATTAN)
|
||||
assert p is not None
|
||||
steps = list(p)
|
||||
assert (int(steps[-1].x), int(steps[-1].y)) == (29, 0)
|
||||
# No step may land on a blocked cell.
|
||||
for s in steps:
|
||||
assert not (int(s.x) == 15 and 5 <= int(s.y) < 25), \
|
||||
f"path stepped through wall at {s}"
|
||||
|
||||
print("PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(0)
|
||||
61
tests/unit/heuristic_enum_test.py
Normal file
61
tests/unit/heuristic_enum_test.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
"""mcrfpy.Heuristic enum exists with expected members and accepts several arg forms."""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
assert hasattr(mcrfpy, "Heuristic"), "mcrfpy.Heuristic missing"
|
||||
H = mcrfpy.Heuristic
|
||||
|
||||
expected = {"EUCLIDEAN": 0, "MANHATTAN": 1, "CHEBYSHEV": 2, "DIAGONAL": 3, "ZERO": 4}
|
||||
for name, value in expected.items():
|
||||
assert hasattr(H, name), f"Heuristic.{name} missing"
|
||||
assert int(getattr(H, name)) == value, f"Heuristic.{name} != {value}"
|
||||
|
||||
members = list(H)
|
||||
assert len(members) == 5, f"expected 5 members, got {len(members)}"
|
||||
|
||||
# find_path accepts enum, int, string
|
||||
g = mcrfpy.Grid(grid_size=(20, 20))
|
||||
for y in range(20):
|
||||
for x in range(20):
|
||||
c = g.at(x, y)
|
||||
c.walkable = True
|
||||
c.transparent = True
|
||||
|
||||
for arg in (H.MANHATTAN, 1, "MANHATTAN"):
|
||||
p = g.find_path((0, 0), (10, 10), heuristic=arg)
|
||||
assert p is not None, f"find_path returned None for heuristic={arg!r}"
|
||||
steps = list(p)
|
||||
assert len(steps) > 0, f"empty path for heuristic={arg!r}"
|
||||
|
||||
# Invalid string raises
|
||||
try:
|
||||
g.find_path((0, 0), (10, 10), heuristic="NOT_A_HEURISTIC")
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected ValueError for bad heuristic string")
|
||||
|
||||
# Invalid int raises
|
||||
try:
|
||||
g.find_path((0, 0), (10, 10), heuristic=99)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected ValueError for out-of-range heuristic int")
|
||||
|
||||
# Non-positive weight raises
|
||||
try:
|
||||
g.find_path((0, 0), (10, 10), weight=0.0)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected ValueError for non-positive weight")
|
||||
|
||||
print("PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(0)
|
||||
83
tests/unit/multi_root_dijkstra_test.py
Normal file
83
tests/unit/multi_root_dijkstra_test.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"""Multi-root Dijkstra distance equals min of per-root distances.
|
||||
|
||||
Also covers the DiscreteMap-mask root-input form introduced in #315.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
def make_grid(w, h):
|
||||
g = mcrfpy.Grid(grid_size=(w, h))
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
c = g.at(x, y)
|
||||
c.walkable = True
|
||||
c.transparent = True
|
||||
return g
|
||||
|
||||
|
||||
def approx(a, b, tol=0.01):
|
||||
return abs(a - b) < tol
|
||||
|
||||
|
||||
def main():
|
||||
g = make_grid(20, 20)
|
||||
roots = [(0, 0), (19, 19), (0, 19)]
|
||||
|
||||
multi = g.get_dijkstra_map(roots=roots)
|
||||
singles = [g.get_dijkstra_map(r) for r in roots]
|
||||
|
||||
# Pick sample cells spread across the grid.
|
||||
samples = [(5, 5), (10, 10), (15, 5), (2, 18), (18, 2), (9, 15)]
|
||||
for p in samples:
|
||||
expected = min(s.distance(p) for s in singles)
|
||||
got = multi.distance(p)
|
||||
assert approx(got, expected), (
|
||||
f"multi-root distance at {p} was {got}, expected {expected}")
|
||||
|
||||
# Distance at each root is 0.
|
||||
for r in roots:
|
||||
assert multi.distance(r) == 0.0, f"root {r} distance should be 0"
|
||||
|
||||
# Single-root via roots= also works.
|
||||
d_single = g.get_dijkstra_map(roots=[(5, 5)])
|
||||
d_ref = g.get_dijkstra_map((5, 5))
|
||||
for p in samples:
|
||||
assert approx(d_single.distance(p), d_ref.distance(p)), \
|
||||
f"single-element roots list diverges at {p}"
|
||||
|
||||
# DiscreteMap mask form: mark four corners, compare against explicit roots.
|
||||
dmap = mcrfpy.DiscreteMap((20, 20))
|
||||
corners = [(0, 0), (19, 0), (0, 19), (19, 19)]
|
||||
for x, y in corners:
|
||||
dmap.set(x, y, 1)
|
||||
|
||||
d_mask = g.get_dijkstra_map(roots=dmap)
|
||||
d_corners = g.get_dijkstra_map(roots=corners)
|
||||
for p in samples:
|
||||
assert approx(d_mask.distance(p), d_corners.distance(p)), \
|
||||
f"mask-root diverges from explicit corners at {p}"
|
||||
|
||||
# Empty mask errors out rather than silently returning all-infinity.
|
||||
empty_mask = mcrfpy.DiscreteMap((20, 20))
|
||||
try:
|
||||
g.get_dijkstra_map(roots=empty_mask)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected ValueError on empty DiscreteMap mask")
|
||||
|
||||
# Passing both root and roots raises.
|
||||
try:
|
||||
g.get_dijkstra_map(root=(0, 0), roots=[(1, 1)])
|
||||
except TypeError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("expected TypeError when both root and roots supplied")
|
||||
|
||||
print("PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(0)
|
||||
Loading…
Add table
Add a link
Reference in a new issue