UIGridPathfinding: clear and separate A-star and Djikstra path systems

This commit is contained in:
John McCardle 2026-01-10 22:09:45 -05:00
commit b32f5af28c
7 changed files with 974 additions and 509 deletions

View file

@ -20,6 +20,7 @@
#include "PyMusic.h"
#include "PyKeyboard.h"
#include "PyMouse.h"
#include "UIGridPathfinding.h" // AStarPath and DijkstraMap types
#include "McRogueFaceVersion.h"
#include "GameEngine.h"
#include "ImGuiConsole.h"
@ -410,6 +411,10 @@ PyObject* PyInit_mcrfpy()
/*mouse state (#186)*/
&PyMouseType,
/*pathfinding result types*/
&mcrfpydef::PyAStarPathType,
&mcrfpydef::PyDijkstraMapType,
nullptr};
// Types that are used internally but NOT exported to module namespace (#189)
@ -422,6 +427,9 @@ PyObject* PyInit_mcrfpy()
&PyUICollectionType, &PyUICollectionIterType,
&PyUIEntityCollectionType, &PyUIEntityCollectionIterType,
/*pathfinding iterator - returned by AStarPath.__iter__() but not directly instantiable*/
&mcrfpydef::PyAStarPathIterType,
nullptr};
// Set up PyWindowType methods and getsetters before PyType_Ready

View file

@ -55,32 +55,6 @@ static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) {
return visible_list;
}
// A* Pathfinding
static PyObject* McRFPy_Libtcod::find_path(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x1, y1, x2, y2;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTuple(args, "Oiiii|f", &grid_obj, &x1, &y1, &x2, &y2, &diagonal_cost)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// Get path from grid
std::vector<std::pair<int, int>> path = grid->findPath(x1, y1, x2, y2, diagonal_cost);
// Convert to Python list
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // steals reference
}
return path_list;
}
// Line drawing algorithm
static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) {
int x1, y1, x2, y2;
@ -112,80 +86,8 @@ static PyObject* McRFPy_Libtcod::line_iter(PyObject* self, PyObject* args) {
return line(self, args);
}
// Dijkstra pathfinding
static PyObject* McRFPy_Libtcod::dijkstra_new(PyObject* self, PyObject* args) {
PyObject* grid_obj;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTuple(args, "O|f", &grid_obj, &diagonal_cost)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
// For now, just return the grid object since Dijkstra is part of the grid
Py_INCREF(grid_obj);
return grid_obj;
}
static PyObject* McRFPy_Libtcod::dijkstra_compute(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int root_x, root_y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &root_x, &root_y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
grid->computeDijkstra(root_x, root_y);
Py_RETURN_NONE;
}
static PyObject* McRFPy_Libtcod::dijkstra_get_distance(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
float distance = grid->getDijkstraDistance(x, y);
if (distance < 0) {
Py_RETURN_NONE;
}
return PyFloat_FromDouble(distance);
}
static PyObject* McRFPy_Libtcod::dijkstra_path_to(PyObject* self, PyObject* args) {
PyObject* grid_obj;
int x, y;
if (!PyArg_ParseTuple(args, "Oii", &grid_obj, &x, &y)) {
return NULL;
}
UIGrid* grid = get_grid_from_pyobject(grid_obj);
if (!grid) return NULL;
std::vector<std::pair<int, int>> path = grid->getDijkstraPath(x, y);
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // steals reference
}
return path_list;
}
// FOV algorithm constants removed - use mcrfpy.FOV enum instead (#114)
// Pathfinding functions removed - use Grid.find_path() and Grid.get_dijkstra_map() instead
// These return AStarPath and DijkstraMap objects (see UIGridPathfinding.h)
// Method definitions
static PyMethodDef libtcodMethods[] = {
@ -201,17 +103,6 @@ static PyMethodDef libtcodMethods[] = {
"Returns:\n"
" List of (x, y) tuples for visible cells"},
{"find_path", McRFPy_Libtcod::find_path, METH_VARARGS,
"find_path(grid, x1, y1, x2, y2, diagonal_cost=1.41)\n\n"
"Find shortest path between two points using A*.\n\n"
"Args:\n"
" grid: Grid object to pathfind on\n"
" x1, y1: Starting position\n"
" x2, y2: Target position\n"
" diagonal_cost: Cost of diagonal movement\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, or empty list if no path exists"},
{"line", McRFPy_Libtcod::line, METH_VARARGS,
"line(x1, y1, x2, y2)\n\n"
"Get cells along a line using Bresenham's algorithm.\n\n"
@ -230,40 +121,6 @@ static PyMethodDef libtcodMethods[] = {
"Returns:\n"
" Iterator of (x, y) tuples along the line"},
{"dijkstra_new", McRFPy_Libtcod::dijkstra_new, METH_VARARGS,
"dijkstra_new(grid, diagonal_cost=1.41)\n\n"
"Create a Dijkstra pathfinding context for a grid.\n\n"
"Args:\n"
" grid: Grid object to use for pathfinding\n"
" diagonal_cost: Cost of diagonal movement\n\n"
"Returns:\n"
" Grid object configured for Dijkstra pathfinding"},
{"dijkstra_compute", McRFPy_Libtcod::dijkstra_compute, METH_VARARGS,
"dijkstra_compute(grid, root_x, root_y)\n\n"
"Compute Dijkstra distance map from root position.\n\n"
"Args:\n"
" grid: Grid object with Dijkstra context\n"
" root_x, root_y: Root position to compute distances from"},
{"dijkstra_get_distance", McRFPy_Libtcod::dijkstra_get_distance, METH_VARARGS,
"dijkstra_get_distance(grid, x, y)\n\n"
"Get distance from root to a position.\n\n"
"Args:\n"
" grid: Grid object with computed Dijkstra map\n"
" x, y: Position to get distance for\n\n"
"Returns:\n"
" Float distance or None if position is invalid/unreachable"},
{"dijkstra_path_to", McRFPy_Libtcod::dijkstra_path_to, METH_VARARGS,
"dijkstra_path_to(grid, x, y)\n\n"
"Get shortest path from position to Dijkstra root.\n\n"
"Args:\n"
" grid: Grid object with computed Dijkstra map\n"
" x, y: Starting position\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path to root"},
{NULL, NULL, 0, NULL}
};
@ -271,7 +128,7 @@ static PyMethodDef libtcodMethods[] = {
static PyModuleDef libtcodModule = {
PyModuleDef_HEAD_INIT,
"mcrfpy.libtcod",
"TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n"
"TCOD-compatible algorithms for field of view and line drawing.\n\n"
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
"FOV Algorithms (use mcrfpy.FOV enum):\n"
@ -281,12 +138,15 @@ static PyModuleDef libtcodModule = {
" mcrfpy.FOV.PERMISSIVE_0 through PERMISSIVE_8 - Permissive variants\n"
" mcrfpy.FOV.RESTRICTIVE - Most restrictive FOV\n"
" mcrfpy.FOV.SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
"Pathfinding:\n"
" Use Grid.find_path() for A* pathfinding (returns AStarPath objects)\n"
" Use Grid.get_dijkstra_map() for Dijkstra pathfinding (returns DijkstraMap objects)\n\n"
"Example:\n"
" import mcrfpy\n"
" from mcrfpy import libtcod\n\n"
" grid = mcrfpy.Grid(50, 50)\n"
" visible = libtcod.compute_fov(grid, 25, 25, 10)\n"
" path = libtcod.find_path(grid, 0, 0, 49, 49)",
" path = grid.find_path((0, 0), (49, 49)) # Returns AStarPath",
-1,
libtcodMethods
};

View file

@ -3,6 +3,7 @@
#include "McRFPy_API.h"
#include <algorithm>
#include <cstring>
#include <libtcod.h>
#include "PyObjectUtils.h"
#include "PyVector.h"
#include "PythonObjectCache.h"
@ -762,23 +763,29 @@ PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kw
return NULL;
}
// Use the grid's Dijkstra implementation
grid->computeDijkstra(current_x, current_y);
auto path = grid->getDijkstraPath(target_x, target_y);
// Use A* pathfinding via temporary TCODPath
TCODPath tcod_path(grid->getTCODMap(), 1.41f);
if (!tcod_path.compute(current_x, current_y, target_x, target_y)) {
// No path found - return empty list
return PyList_New(0);
}
// Convert path to Python list of tuples
PyObject* path_list = PyList_New(path.size());
PyObject* path_list = PyList_New(tcod_path.size());
if (!path_list) return PyErr_NoMemory();
for (size_t i = 0; i < path.size(); ++i) {
for (int i = 0; i < tcod_path.size(); ++i) {
int px, py;
tcod_path.get(i, &px, &py);
PyObject* coord_tuple = PyTuple_New(2);
if (!coord_tuple) {
Py_DECREF(path_list);
return PyErr_NoMemory();
}
PyTuple_SetItem(coord_tuple, 0, PyLong_FromLong(path[i].first));
PyTuple_SetItem(coord_tuple, 1, PyLong_FromLong(path[i].second));
PyTuple_SetItem(coord_tuple, 0, PyLong_FromLong(px));
PyTuple_SetItem(coord_tuple, 1, PyLong_FromLong(py));
PyList_SetItem(path_list, i, coord_tuple);
}

View file

@ -1,4 +1,5 @@
#include "UIGrid.h"
#include "UIGridPathfinding.h" // New pathfinding API
#include "GameEngine.h"
#include "McRFPy_API.h"
#include "PythonObjectCache.h"
@ -17,7 +18,7 @@
UIGrid::UIGrid()
: grid_w(0), grid_h(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
fill_color(8, 8, 8, 255), tcod_map(nullptr),
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
use_chunks(false) // Default to omniscient view
{
@ -49,7 +50,7 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
: grid_w(gx), grid_h(gy),
zoom(1.0f),
ptex(_ptex),
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
fill_color(8, 8, 8, 255), tcod_map(nullptr),
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
use_chunks(gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD) // #123 - Use chunks for large grids
{
@ -84,14 +85,10 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
// textures are upside-down inside renderTexture
output.setTexture(renderTexture.getTexture());
// Create TCOD map
// Create TCOD map for FOV and as source for pathfinding
tcod_map = new TCODMap(gx, gy);
// Create TCOD dijkstra pathfinder
tcod_dijkstra = new TCODDijkstra(tcod_map);
// Create TCOD A* pathfinder
tcod_path = new TCODPath(tcod_map);
// Note: DijkstraMap objects are created on-demand via get_dijkstra_map()
// A* paths are computed on-demand via find_path()
// #123 - Initialize storage based on grid size
if (use_chunks) {
@ -368,14 +365,9 @@ UIGridPoint& UIGrid::at(int x, int y)
UIGrid::~UIGrid()
{
if (tcod_path) {
delete tcod_path;
tcod_path = nullptr;
}
if (tcod_dijkstra) {
delete tcod_dijkstra;
tcod_dijkstra = nullptr;
}
// Clear Dijkstra maps first (they reference tcod_map)
dijkstra_maps.clear();
if (tcod_map) {
delete tcod_map;
tcod_map = nullptr;
@ -476,98 +468,9 @@ bool UIGrid::isInFOV(int x, int y) const
return tcod_map->isInFov(x, y);
}
std::vector<std::pair<int, int>> UIGrid::findPath(int x1, int y1, int x2, int y2, float diagonalCost)
{
std::vector<std::pair<int, int>> path;
if (!tcod_map || x1 < 0 || x1 >= grid_w || y1 < 0 || y1 >= grid_h ||
x2 < 0 || x2 >= grid_w || y2 < 0 || y2 >= grid_h) {
return path;
}
TCODPath tcod_path(tcod_map, diagonalCost);
if (tcod_path.compute(x1, y1, x2, y2)) {
for (int i = 0; i < tcod_path.size(); i++) {
int x, y;
tcod_path.get(i, &x, &y);
path.push_back(std::make_pair(x, y));
}
}
return path;
}
void UIGrid::computeDijkstra(int rootX, int rootY, float diagonalCost)
{
if (!tcod_map || !tcod_dijkstra || rootX < 0 || rootX >= grid_w || rootY < 0 || rootY >= grid_h) return;
// Compute the Dijkstra map from the root position
tcod_dijkstra->compute(rootX, rootY);
}
float UIGrid::getDijkstraDistance(int x, int y) const
{
if (!tcod_dijkstra || x < 0 || x >= grid_w || y < 0 || y >= grid_h) {
return -1.0f; // Invalid position
}
return tcod_dijkstra->getDistance(x, y);
}
std::vector<std::pair<int, int>> UIGrid::getDijkstraPath(int x, int y) const
{
std::vector<std::pair<int, int>> path;
if (!tcod_dijkstra || x < 0 || x >= grid_w || y < 0 || y >= grid_h) {
return path; // Empty path for invalid position
}
// Set the destination
if (tcod_dijkstra->setPath(x, y)) {
// Walk the path and collect points
int px, py;
while (tcod_dijkstra->walk(&px, &py)) {
path.push_back(std::make_pair(px, py));
}
}
return path;
}
// A* pathfinding implementation
std::vector<std::pair<int, int>> UIGrid::computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost)
{
std::vector<std::pair<int, int>> path;
// Validate inputs
if (!tcod_map || !tcod_path ||
x1 < 0 || x1 >= grid_w || y1 < 0 || y1 >= grid_h ||
x2 < 0 || x2 >= grid_w || y2 < 0 || y2 >= grid_h) {
return path; // Return empty path
}
// Set diagonal cost (TCODPath doesn't take it as parameter to compute)
// Instead, diagonal cost is set during TCODPath construction
// For now, we'll use the default diagonal cost from the constructor
// Compute the path
bool success = tcod_path->compute(x1, y1, x2, y2);
if (success) {
// Get the computed path
int pathSize = tcod_path->size();
path.reserve(pathSize);
// TCOD path includes the starting position, so we start from index 0
for (int i = 0; i < pathSize; i++) {
int px, py;
tcod_path->get(i, &px, &py);
path.push_back(std::make_pair(px, py));
}
}
return path;
}
// Pathfinding methods moved to UIGridPathfinding.cpp
// - Grid.find_path() returns AStarPath objects
// - Grid.get_dijkstra_map() returns DijkstraMap objects (cached)
// Phase 1 implementations
sf::FloatRect UIGrid::get_bounds() const
@ -1453,128 +1356,9 @@ PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* k
return PyBool_FromLong(in_fov);
}
PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL};
PyObject* start_obj = NULL;
PyObject* end_obj = NULL;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast<char**>(kwlist),
&start_obj, &end_obj, &diagonal_cost)) {
return NULL;
}
int x1, y1, x2, y2;
if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) {
return NULL;
}
if (!PyPosition_FromObjectInt(end_obj, &x2, &y2)) {
return NULL;
}
std::vector<std::pair<int, int>> path = self->data->findPath(x1, y1, x2, y2, diagonal_cost);
PyObject* path_list = PyList_New(path.size());
if (!path_list) return NULL;
for (size_t i = 0; i < path.size(); i++) {
PyObject* coord = Py_BuildValue("(ii)", path[i].first, path[i].second);
if (!coord) {
Py_DECREF(path_list);
return NULL;
}
PyList_SET_ITEM(path_list, i, coord);
}
return path_list;
}
PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"root", "diagonal_cost", NULL};
PyObject* root_obj = NULL;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(kwlist),
&root_obj, &diagonal_cost)) {
return NULL;
}
int root_x, root_y;
if (!PyPosition_FromObjectInt(root_obj, &root_x, &root_y)) {
return NULL;
}
self->data->computeDijkstra(root_x, root_y, diagonal_cost);
Py_RETURN_NONE;
}
PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
int x, y;
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL;
}
float distance = self->data->getDijkstraDistance(x, y);
if (distance < 0) {
Py_RETURN_NONE; // Invalid position
}
return PyFloat_FromDouble(distance);
}
PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
int x, y;
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL;
}
std::vector<std::pair<int, int>> path = self->data->getDijkstraPath(x, y);
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // Steals reference
}
return path_list;
}
PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL};
PyObject* start_obj = NULL;
PyObject* end_obj = NULL;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast<char**>(kwlist),
&start_obj, &end_obj, &diagonal_cost)) {
return NULL;
}
int x1, y1, x2, y2;
if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) {
return NULL;
}
if (!PyPosition_FromObjectInt(end_obj, &x2, &y2)) {
return NULL;
}
// Compute A* path
std::vector<std::pair<int, int>> path = self->data->computeAStarPath(x1, y1, x2, y2, diagonal_cost);
// Convert to Python list
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // Steals reference
}
return path_list;
}
// Old pathfinding Python methods removed - see UIGridPathfinding.cpp for new implementation
// Grid.find_path() now returns AStarPath objects
// Grid.get_dijkstra_map() returns DijkstraMap objects (cached by root)
// #147 - Layer system Python API
PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
@ -1925,51 +1709,32 @@ PyMethodDef UIGrid::methods[] = {
"Returns:\n"
" True if the cell is visible, False otherwise\n\n"
"Must call compute_fov() first to calculate visibility."},
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Find A* path between two points.\n\n"
"Args:\n"
" start: Starting position as (x, y) tuple, list, or Vector\n"
" end: Target position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
"Uses A* algorithm with walkability from grid cells."},
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
"compute_dijkstra(root, diagonal_cost: float = 1.41) -> None\n\n"
"Compute Dijkstra map from root position.\n\n"
"Args:\n"
" root: Root position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Precomputes distances from all reachable cells to the root.\n"
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
"Useful for multiple entities pathfinding to the same target."},
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_distance(pos) -> Optional[float]\n\n"
"Get distance from Dijkstra root to position.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" Distance as float, or None if position is unreachable or invalid\n\n"
"Must call compute_dijkstra() first."},
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n"
"Get path from position to Dijkstra root.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" List of (x, y) tuples representing path to root, empty if unreachable\n\n"
"Must call compute_dijkstra() first. Path includes start but not root position."},
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
"compute_astar_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
{"find_path", (PyCFunction)UIGridPathfinding::Grid_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(start, end, diagonal_cost: float = 1.41) -> AStarPath | None\n\n"
"Compute A* path between two points.\n\n"
"Args:\n"
" start: Starting position as (x, y) tuple, list, or Vector\n"
" end: Target position as (x, y) tuple, list, or Vector\n"
" start: Starting position as Vector, Entity, or (x, y) tuple\n"
" end: Target position as Vector, Entity, or (x, y) tuple\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
"Alternative A* implementation. Prefer find_path() for consistency."},
" AStarPath object if path exists, None otherwise.\n\n"
"The returned AStarPath can be iterated or walked step-by-step."},
{"get_dijkstra_map", (PyCFunction)UIGridPathfinding::Grid_get_dijkstra_map, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_map(root, diagonal_cost: float = 1.41) -> DijkstraMap\n\n"
"Get or create a Dijkstra distance map for a root position.\n\n"
"Args:\n"
" root: Root position as Vector, Entity, or (x, y) tuple\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" DijkstraMap object for querying distances and paths.\n\n"
"Grid caches DijkstraMaps by root position. Multiple requests for the\n"
"same root return the same cached map. Call clear_dijkstra_maps() after\n"
"changing grid walkability to invalidate the cache."},
{"clear_dijkstra_maps", (PyCFunction)UIGridPathfinding::Grid_clear_dijkstra_maps, METH_NOARGS,
"clear_dijkstra_maps() -> None\n\n"
"Clear all cached Dijkstra maps.\n\n"
"Call this after modifying grid cell walkability to ensure pathfinding\n"
"uses updated walkability data."},
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS | METH_KEYWORDS,
"add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer"},
{"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS,
@ -2021,51 +1786,32 @@ PyMethodDef UIGrid_all_methods[] = {
"Returns:\n"
" True if the cell is visible, False otherwise\n\n"
"Must call compute_fov() first to calculate visibility."},
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Find A* path between two points.\n\n"
"Args:\n"
" start: Starting position as (x, y) tuple, list, or Vector\n"
" end: Target position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
"Uses A* algorithm with walkability from grid cells."},
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
"compute_dijkstra(root, diagonal_cost: float = 1.41) -> None\n\n"
"Compute Dijkstra map from root position.\n\n"
"Args:\n"
" root: Root position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Precomputes distances from all reachable cells to the root.\n"
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
"Useful for multiple entities pathfinding to the same target."},
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_distance(pos) -> Optional[float]\n\n"
"Get distance from Dijkstra root to position.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" Distance as float, or None if position is unreachable or invalid\n\n"
"Must call compute_dijkstra() first."},
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n"
"Get path from position to Dijkstra root.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" List of (x, y) tuples representing path to root, empty if unreachable\n\n"
"Must call compute_dijkstra() first. Path includes start but not root position."},
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
"compute_astar_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
{"find_path", (PyCFunction)UIGridPathfinding::Grid_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(start, end, diagonal_cost: float = 1.41) -> AStarPath | None\n\n"
"Compute A* path between two points.\n\n"
"Args:\n"
" start: Starting position as (x, y) tuple, list, or Vector\n"
" end: Target position as (x, y) tuple, list, or Vector\n"
" start: Starting position as Vector, Entity, or (x, y) tuple\n"
" end: Target position as Vector, Entity, or (x, y) tuple\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
"Alternative A* implementation. Prefer find_path() for consistency."},
" AStarPath object if path exists, None otherwise.\n\n"
"The returned AStarPath can be iterated or walked step-by-step."},
{"get_dijkstra_map", (PyCFunction)UIGridPathfinding::Grid_get_dijkstra_map, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_map(root, diagonal_cost: float = 1.41) -> DijkstraMap\n\n"
"Get or create a Dijkstra distance map for a root position.\n\n"
"Args:\n"
" root: Root position as Vector, Entity, or (x, y) tuple\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" DijkstraMap object for querying distances and paths.\n\n"
"Grid caches DijkstraMaps by root position. Multiple requests for the\n"
"same root return the same cached map. Call clear_dijkstra_maps() after\n"
"changing grid walkability to invalidate the cache."},
{"clear_dijkstra_maps", (PyCFunction)UIGridPathfinding::Grid_clear_dijkstra_maps, METH_NOARGS,
"clear_dijkstra_maps() -> None\n\n"
"Clear all cached Dijkstra maps.\n\n"
"Call this after modifying grid cell walkability to ensure pathfinding\n"
"uses updated walkability data."},
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS | METH_KEYWORDS,
"add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer\n\n"
"Add a new layer to the grid.\n\n"

View file

@ -8,6 +8,8 @@
#include <libtcod.h>
#include <mutex>
#include <optional>
#include <map>
#include <memory>
#include "PyCallable.h"
#include "PyTexture.h"
@ -25,6 +27,9 @@
#include "SpatialHash.h"
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
// Forward declaration for pathfinding
class DijkstraMap;
class UIGrid: public UIDrawable
{
private:
@ -33,10 +38,13 @@ private:
static constexpr int DEFAULT_CELL_WIDTH = 16;
static constexpr int DEFAULT_CELL_HEIGHT = 16;
TCODMap* tcod_map; // TCOD map for FOV and pathfinding
TCODDijkstra* tcod_dijkstra; // Dijkstra pathfinding
TCODPath* tcod_path; // A* pathfinding
mutable std::mutex fov_mutex; // Mutex for thread-safe FOV operations
public:
// Dijkstra map cache - keyed by root position
// Public so UIGridPathfinding can access it
std::map<std::pair<int,int>, std::shared_ptr<DijkstraMap>> dijkstra_maps;
public:
UIGrid();
//UIGrid(int, int, IndexTexture*, float, float, float, float);
@ -54,15 +62,12 @@ public:
void syncTCODMapCell(int x, int y); // Sync a single cell to TCOD map
void computeFOV(int x, int y, int radius, bool light_walls = true, TCOD_fov_algorithm_t algo = FOV_BASIC);
bool isInFOV(int x, int y) const;
TCODMap* getTCODMap() const { return tcod_map; } // Access for pathfinding
// Pathfinding methods
std::vector<std::pair<int, int>> findPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f);
void computeDijkstra(int rootX, int rootY, float diagonalCost = 1.41f);
float getDijkstraDistance(int x, int y) const;
std::vector<std::pair<int, int>> getDijkstraPath(int x, int y) const;
// A* pathfinding methods
std::vector<std::pair<int, int>> computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f);
// Pathfinding - new API creates AStarPath/DijkstraMap objects
// See UIGridPathfinding.h for the new pathfinding API
// Grid.find_path() now returns AStarPath objects
// Grid.get_dijkstra_map() returns DijkstraMap objects (cached by root position)
// Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override;
@ -167,11 +172,10 @@ public:
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
// Pathfinding methods moved to UIGridPathfinding.cpp
// py_find_path -> UIGridPathfinding::Grid_find_path (returns AStarPath)
// py_get_dijkstra_map -> UIGridPathfinding::Grid_get_dijkstra_map (returns DijkstraMap)
// py_clear_dijkstra_maps -> UIGridPathfinding::Grid_clear_dijkstra_maps
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169

688
src/UIGridPathfinding.cpp Normal file
View file

@ -0,0 +1,688 @@
#include "UIGridPathfinding.h"
#include "UIGrid.h"
#include "UIEntity.h"
#include "PyVector.h"
#include "McRFPy_API.h"
//=============================================================================
// DijkstraMap Implementation
//=============================================================================
DijkstraMap::DijkstraMap(TCODMap* map, int root_x, int root_y, float diag_cost)
: tcod_map(map)
, root(root_x, root_y)
, diagonal_cost(diag_cost)
{
tcod_dijkstra = new TCODDijkstra(tcod_map, diagonal_cost);
tcod_dijkstra->compute(root_x, root_y); // Compute immediately at creation
}
DijkstraMap::~DijkstraMap() {
if (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);
}
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)) {
int px, py;
while (tcod_dijkstra->walk(&px, &py)) {
path.push_back(sf::Vector2i(px, py));
}
}
return path;
}
sf::Vector2i DijkstraMap::stepFrom(int x, int y, bool* valid) const {
if (!tcod_dijkstra) {
if (valid) *valid = false;
return sf::Vector2i(-1, -1);
}
if (!tcod_dijkstra->setPath(x, y)) {
if (valid) *valid = false;
return sf::Vector2i(-1, -1);
}
int px, py;
if (tcod_dijkstra->walk(&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);
}
//=============================================================================
// Helper Functions
//=============================================================================
bool UIGridPathfinding::ExtractPosition(PyObject* obj, int* x, int* y,
UIGrid* expected_grid,
const char* arg_name) {
// Get types from module to avoid static type instance issues
PyObject* entity_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
// Check if it's an Entity
if (entity_type && PyObject_IsInstance(obj, entity_type)) {
Py_DECREF(entity_type);
Py_XDECREF(vector_type);
auto* entity = (PyUIEntityObject*)obj;
if (!entity->data) {
PyErr_Format(PyExc_RuntimeError,
"%s: Entity has no data", arg_name);
return false;
}
if (!entity->data->grid) {
PyErr_Format(PyExc_RuntimeError,
"%s: Entity is not attached to any grid", arg_name);
return false;
}
if (expected_grid && entity->data->grid.get() != expected_grid) {
PyErr_Format(PyExc_RuntimeError,
"%s: Entity belongs to a different grid", arg_name);
return false;
}
*x = static_cast<int>(entity->data->position.x);
*y = static_cast<int>(entity->data->position.y);
return true;
}
Py_XDECREF(entity_type);
// Check if it's a Vector
if (vector_type && PyObject_IsInstance(obj, vector_type)) {
Py_DECREF(vector_type);
auto* vec = (PyVectorObject*)obj;
*x = static_cast<int>(vec->data.x);
*y = static_cast<int>(vec->data.y);
return true;
}
Py_XDECREF(vector_type);
// Try tuple/list
if (PySequence_Check(obj) && PySequence_Size(obj) == 2) {
PyObject* x_obj = PySequence_GetItem(obj, 0);
PyObject* y_obj = PySequence_GetItem(obj, 1);
bool ok = false;
if (x_obj && y_obj && PyNumber_Check(x_obj) && PyNumber_Check(y_obj)) {
PyObject* x_long = PyNumber_Long(x_obj);
PyObject* y_long = PyNumber_Long(y_obj);
if (x_long && y_long) {
*x = PyLong_AsLong(x_long);
*y = PyLong_AsLong(y_long);
ok = !PyErr_Occurred();
Py_DECREF(x_long);
Py_DECREF(y_long);
}
Py_XDECREF(x_long);
Py_XDECREF(y_long);
}
Py_XDECREF(x_obj);
Py_XDECREF(y_obj);
if (ok) return true;
}
PyErr_Format(PyExc_TypeError,
"%s: expected Vector, Entity, or (x, y) tuple", arg_name);
return false;
}
//=============================================================================
// AStarPath Python Methods
//=============================================================================
PyObject* UIGridPathfinding::AStarPath_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyAStarPathObject* self = (PyAStarPathObject*)type->tp_alloc(type, 0);
if (self) {
new (&self->path) std::vector<sf::Vector2i>(); // Placement new
self->current_index = 0;
self->origin = sf::Vector2i(0, 0);
self->destination = sf::Vector2i(0, 0);
}
return (PyObject*)self;
}
int UIGridPathfinding::AStarPath_init(PyAStarPathObject* self, PyObject* args, PyObject* kwds) {
// AStarPath should not be created directly from Python
PyErr_SetString(PyExc_TypeError,
"AStarPath cannot be instantiated directly. Use Grid.find_path() instead.");
return -1;
}
void UIGridPathfinding::AStarPath_dealloc(PyAStarPathObject* self) {
self->path.~vector(); // Explicitly destroy
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* UIGridPathfinding::AStarPath_repr(PyAStarPathObject* self) {
size_t remaining = self->path.size() - self->current_index;
return PyUnicode_FromFormat("<AStarPath from (%d,%d) to (%d,%d), %zu steps remaining>",
self->origin.x, self->origin.y,
self->destination.x, self->destination.y,
remaining);
}
PyObject* UIGridPathfinding::AStarPath_walk(PyAStarPathObject* self, PyObject* args) {
if (self->current_index >= self->path.size()) {
PyErr_SetString(PyExc_IndexError, "Path exhausted - no more steps");
return NULL;
}
sf::Vector2i pos = self->path[self->current_index++];
return PyVector(sf::Vector2f(static_cast<float>(pos.x), static_cast<float>(pos.y))).pyObject();
}
PyObject* UIGridPathfinding::AStarPath_peek(PyAStarPathObject* self, PyObject* args) {
if (self->current_index >= self->path.size()) {
PyErr_SetString(PyExc_IndexError, "Path exhausted - no more steps");
return NULL;
}
sf::Vector2i pos = self->path[self->current_index];
return PyVector(sf::Vector2f(static_cast<float>(pos.x), static_cast<float>(pos.y))).pyObject();
}
PyObject* UIGridPathfinding::AStarPath_get_origin(PyAStarPathObject* self, void* closure) {
return PyVector(sf::Vector2f(static_cast<float>(self->origin.x),
static_cast<float>(self->origin.y))).pyObject();
}
PyObject* UIGridPathfinding::AStarPath_get_destination(PyAStarPathObject* self, void* closure) {
return PyVector(sf::Vector2f(static_cast<float>(self->destination.x),
static_cast<float>(self->destination.y))).pyObject();
}
PyObject* UIGridPathfinding::AStarPath_get_remaining(PyAStarPathObject* self, void* closure) {
size_t remaining = self->path.size() - self->current_index;
return PyLong_FromSize_t(remaining);
}
Py_ssize_t UIGridPathfinding::AStarPath_len(PyAStarPathObject* self) {
return static_cast<Py_ssize_t>(self->path.size() - self->current_index);
}
int UIGridPathfinding::AStarPath_bool(PyObject* obj) {
PyAStarPathObject* self = (PyAStarPathObject*)obj;
return self->current_index < self->path.size() ? 1 : 0;
}
PyObject* UIGridPathfinding::AStarPath_iter(PyAStarPathObject* self) {
// Create iterator object
mcrfpydef::PyAStarPathIterObject* iter = PyObject_New(
mcrfpydef::PyAStarPathIterObject, &mcrfpydef::PyAStarPathIterType);
if (!iter) return NULL;
Py_INCREF(self);
iter->path = self;
iter->iter_index = self->current_index;
return (PyObject*)iter;
}
// Iterator implementation
static void AStarPathIter_dealloc(mcrfpydef::PyAStarPathIterObject* self) {
Py_XDECREF(self->path);
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject* AStarPathIter_next(mcrfpydef::PyAStarPathIterObject* self) {
if (!self->path || self->iter_index >= self->path->path.size()) {
return NULL; // StopIteration
}
sf::Vector2i pos = self->path->path[self->iter_index++];
// Note: Iterating is consuming for this iterator
self->path->current_index = self->iter_index;
return PyVector(sf::Vector2f(static_cast<float>(pos.x), static_cast<float>(pos.y))).pyObject();
}
static PyObject* AStarPathIter_iter(mcrfpydef::PyAStarPathIterObject* self) {
Py_INCREF(self);
return (PyObject*)self;
}
//=============================================================================
// DijkstraMap Python Methods
//=============================================================================
PyObject* UIGridPathfinding::DijkstraMap_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
PyDijkstraMapObject* self = (PyDijkstraMapObject*)type->tp_alloc(type, 0);
if (self) {
new (&self->data) std::shared_ptr<DijkstraMap>();
}
return (PyObject*)self;
}
int UIGridPathfinding::DijkstraMap_init(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
PyErr_SetString(PyExc_TypeError,
"DijkstraMap cannot be instantiated directly. Use Grid.get_dijkstra_map() instead.");
return -1;
}
void UIGridPathfinding::DijkstraMap_dealloc(PyDijkstraMapObject* self) {
self->data.~shared_ptr();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* UIGridPathfinding::DijkstraMap_repr(PyDijkstraMapObject* self) {
if (!self->data) {
return PyUnicode_FromString("<DijkstraMap (invalid)>");
}
sf::Vector2i root = self->data->getRoot();
return PyUnicode_FromFormat("<DijkstraMap root=(%d,%d)>", root.x, root.y);
}
PyObject* UIGridPathfinding::DijkstraMap_distance(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"pos", NULL};
PyObject* pos_obj = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(kwlist), &pos_obj)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid");
return NULL;
}
int x, y;
if (!ExtractPosition(pos_obj, &x, &y, nullptr, "pos")) {
return NULL;
}
float dist = self->data->getDistance(x, y);
if (dist < 0) {
Py_RETURN_NONE; // Unreachable
}
return PyFloat_FromDouble(dist);
}
PyObject* UIGridPathfinding::DijkstraMap_path_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"pos", NULL};
PyObject* pos_obj = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(kwlist), &pos_obj)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid");
return NULL;
}
int x, y;
if (!ExtractPosition(pos_obj, &x, &y, nullptr, "pos")) {
return NULL;
}
std::vector<sf::Vector2i> path = self->data->getPathFrom(x, y);
// Create an AStarPath object to return
PyAStarPathObject* result = (PyAStarPathObject*)mcrfpydef::PyAStarPathType.tp_alloc(
&mcrfpydef::PyAStarPathType, 0);
if (!result) return NULL;
new (&result->path) std::vector<sf::Vector2i>(std::move(path));
result->current_index = 0;
result->origin = sf::Vector2i(x, y);
result->destination = self->data->getRoot();
return (PyObject*)result;
}
PyObject* UIGridPathfinding::DijkstraMap_step_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"pos", NULL};
PyObject* pos_obj = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(kwlist), &pos_obj)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid");
return NULL;
}
int x, y;
if (!ExtractPosition(pos_obj, &x, &y, nullptr, "pos")) {
return NULL;
}
bool valid = false;
sf::Vector2i step = self->data->stepFrom(x, y, &valid);
if (!valid) {
Py_RETURN_NONE; // At root or unreachable
}
return PyVector(sf::Vector2f(static_cast<float>(step.x), static_cast<float>(step.y))).pyObject();
}
PyObject* UIGridPathfinding::DijkstraMap_get_root(PyDijkstraMapObject* self, void* closure) {
if (!self->data) {
Py_RETURN_NONE;
}
sf::Vector2i root = self->data->getRoot();
return PyVector(sf::Vector2f(static_cast<float>(root.x), static_cast<float>(root.y))).pyObject();
}
//=============================================================================
// Grid Factory Methods
//=============================================================================
PyObject* UIGridPathfinding::Grid_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL};
PyObject* start_obj = NULL;
PyObject* end_obj = NULL;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast<char**>(kwlist),
&start_obj, &end_obj, &diagonal_cost)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Grid is invalid");
return NULL;
}
int x1, y1, x2, y2;
if (!ExtractPosition(start_obj, &x1, &y1, self->data.get(), "start")) {
return NULL;
}
if (!ExtractPosition(end_obj, &x2, &y2, self->data.get(), "end")) {
return NULL;
}
// Bounds check
if (x1 < 0 || x1 >= self->data->grid_w || y1 < 0 || y1 >= self->data->grid_h ||
x2 < 0 || x2 >= self->data->grid_w || y2 < 0 || y2 >= self->data->grid_h) {
PyErr_SetString(PyExc_ValueError, "Position out of grid bounds");
return NULL;
}
// Compute path using temporary TCODPath
TCODPath tcod_path(self->data->getTCODMap(), diagonal_cost);
if (!tcod_path.compute(x1, y1, x2, y2)) {
Py_RETURN_NONE; // No path exists
}
// Create AStarPath result object
PyAStarPathObject* result = (PyAStarPathObject*)mcrfpydef::PyAStarPathType.tp_alloc(
&mcrfpydef::PyAStarPathType, 0);
if (!result) 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 px, py;
tcod_path.get(i, &px, &py);
result->path.push_back(sf::Vector2i(px, py));
}
return (PyObject*)result;
}
PyObject* UIGridPathfinding::Grid_get_dijkstra_map(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"root", "diagonal_cost", NULL};
PyObject* root_obj = NULL;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(kwlist),
&root_obj, &diagonal_cost)) {
return NULL;
}
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Grid is invalid");
return NULL;
}
int root_x, root_y;
if (!ExtractPosition(root_obj, &root_x, &root_y, self->data.get(), "root")) {
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");
return NULL;
}
auto key = std::make_pair(root_x, root_y);
// Check cache
auto it = self->data->dijkstra_maps.find(key);
if (it != self->data->dijkstra_maps.end()) {
// Check diagonal cost matches (or we could ignore this)
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;
}
// Different diagonal cost - remove old one
self->data->dijkstra_maps.erase(it);
}
// Create new DijkstraMap
auto dijkstra = std::make_shared<DijkstraMap>(
self->data->getTCODMap(), root_x, root_y, diagonal_cost);
// Cache it
self->data->dijkstra_maps[key] = dijkstra;
// Return Python wrapper
PyDijkstraMapObject* result = (PyDijkstraMapObject*)mcrfpydef::PyDijkstraMapType.tp_alloc(
&mcrfpydef::PyDijkstraMapType, 0);
if (!result) return NULL;
new (&result->data) std::shared_ptr<DijkstraMap>(dijkstra);
return (PyObject*)result;
}
PyObject* UIGridPathfinding::Grid_clear_dijkstra_maps(PyUIGridObject* self, PyObject* args) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Grid is invalid");
return NULL;
}
self->data->dijkstra_maps.clear();
Py_RETURN_NONE;
}
//=============================================================================
// Python Type Definitions
//=============================================================================
namespace mcrfpydef {
// AStarPath methods
PyMethodDef PyAStarPath_methods[] = {
{"walk", (PyCFunction)UIGridPathfinding::AStarPath_walk, METH_NOARGS,
"walk() -> Vector\n\n"
"Get and consume next step in the path.\n\n"
"Returns:\n"
" Next position as Vector.\n\n"
"Raises:\n"
" IndexError: If path is exhausted."},
{"peek", (PyCFunction)UIGridPathfinding::AStarPath_peek, METH_NOARGS,
"peek() -> Vector\n\n"
"See next step without consuming it.\n\n"
"Returns:\n"
" Next position as Vector.\n\n"
"Raises:\n"
" IndexError: If path is exhausted."},
{NULL}
};
// AStarPath getsetters
PyGetSetDef PyAStarPath_getsetters[] = {
{"origin", (getter)UIGridPathfinding::AStarPath_get_origin, NULL,
"Starting position of the path (Vector, read-only).", NULL},
{"destination", (getter)UIGridPathfinding::AStarPath_get_destination, NULL,
"Ending position of the path (Vector, read-only).", NULL},
{"remaining", (getter)UIGridPathfinding::AStarPath_get_remaining, NULL,
"Number of steps remaining in the path (int, read-only).", NULL},
{NULL}
};
// AStarPath number methods (for bool)
PyNumberMethods PyAStarPath_as_number = {
.nb_bool = UIGridPathfinding::AStarPath_bool,
};
// AStarPath sequence methods (for len)
PySequenceMethods PyAStarPath_as_sequence = {
.sq_length = (lenfunc)UIGridPathfinding::AStarPath_len,
};
// AStarPath type
PyTypeObject PyAStarPathType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.AStarPath",
.tp_basicsize = sizeof(PyAStarPathObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)UIGridPathfinding::AStarPath_dealloc,
.tp_repr = (reprfunc)UIGridPathfinding::AStarPath_repr,
.tp_as_number = &PyAStarPath_as_number,
.tp_as_sequence = &PyAStarPath_as_sequence,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"A computed A* path result, consumed step by step.\n\n"
"Created by Grid.find_path(). Cannot be instantiated directly.\n\n"
"Use walk() to get and consume each step, or iterate directly.\n"
"Use peek() to see the next step without consuming it.\n"
"Use bool(path) or len(path) to check if steps remain.\n\n"
"Properties:\n"
" origin (Vector): Starting position (read-only)\n"
" destination (Vector): Ending position (read-only)\n"
" remaining (int): Steps remaining (read-only)\n\n"
"Example:\n"
" path = grid.find_path(start, end)\n"
" if path:\n"
" while path:\n"
" next_pos = path.walk()\n"
" entity.pos = next_pos"),
.tp_iter = (getiterfunc)UIGridPathfinding::AStarPath_iter,
.tp_methods = PyAStarPath_methods,
.tp_getset = PyAStarPath_getsetters,
.tp_init = (initproc)UIGridPathfinding::AStarPath_init,
.tp_new = UIGridPathfinding::AStarPath_new,
};
// AStarPath iterator type
PyTypeObject PyAStarPathIterType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.AStarPathIterator",
.tp_basicsize = sizeof(PyAStarPathIterObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)AStarPathIter_dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_iter = (getiterfunc)AStarPathIter_iter,
.tp_iternext = (iternextfunc)AStarPathIter_next,
};
// DijkstraMap methods
PyMethodDef PyDijkstraMap_methods[] = {
{"distance", (PyCFunction)UIGridPathfinding::DijkstraMap_distance, METH_VARARGS | METH_KEYWORDS,
"distance(pos) -> float | None\n\n"
"Get distance from position to root.\n\n"
"Args:\n"
" pos: Position as Vector, Entity, or (x, y) tuple.\n\n"
"Returns:\n"
" Float distance, or None if position is unreachable."},
{"path_from", (PyCFunction)UIGridPathfinding::DijkstraMap_path_from, METH_VARARGS | METH_KEYWORDS,
"path_from(pos) -> AStarPath\n\n"
"Get full path from position to root.\n\n"
"Args:\n"
" pos: Starting position as Vector, Entity, or (x, y) tuple.\n\n"
"Returns:\n"
" AStarPath from pos toward root."},
{"step_from", (PyCFunction)UIGridPathfinding::DijkstraMap_step_from, METH_VARARGS | METH_KEYWORDS,
"step_from(pos) -> Vector | None\n\n"
"Get single step from position toward root.\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 at root or unreachable."},
{NULL}
};
// DijkstraMap getsetters
PyGetSetDef PyDijkstraMap_getsetters[] = {
{"root", (getter)UIGridPathfinding::DijkstraMap_get_root, NULL,
"Root position that distances are measured from (Vector, read-only).", NULL},
{NULL}
};
// DijkstraMap type
PyTypeObject PyDijkstraMapType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.DijkstraMap",
.tp_basicsize = sizeof(PyDijkstraMapObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)UIGridPathfinding::DijkstraMap_dealloc,
.tp_repr = (reprfunc)UIGridPathfinding::DijkstraMap_repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"A Dijkstra distance map from a fixed root position.\n\n"
"Created by Grid.get_dijkstra_map(). Cannot be instantiated directly.\n\n"
"Grid caches these maps - multiple requests for the same root return\n"
"the same map. Call Grid.clear_dijkstra_maps() after changing grid\n"
"walkability to invalidate the cache.\n\n"
"Properties:\n"
" root (Vector): Root position (read-only)\n\n"
"Methods:\n"
" distance(pos) -> float | None: Get distance to root\n"
" path_from(pos) -> AStarPath: Get full path to root\n"
" step_from(pos) -> Vector | None: Get single step toward root\n\n"
"Example:\n"
" dijkstra = grid.get_dijkstra_map(player.pos)\n"
" for enemy in enemies:\n"
" dist = dijkstra.distance(enemy.pos)\n"
" if dist and dist < 10:\n"
" step = dijkstra.step_from(enemy.pos)\n"
" if step:\n"
" enemy.pos = step"),
.tp_methods = PyDijkstraMap_methods,
.tp_getset = PyDijkstraMap_getsetters,
.tp_init = (initproc)UIGridPathfinding::DijkstraMap_init,
.tp_new = UIGridPathfinding::DijkstraMap_new,
};
} // namespace mcrfpydef

152
src/UIGridPathfinding.h Normal file
View file

@ -0,0 +1,152 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "UIBase.h" // For PyUIGridObject typedef
#include <libtcod.h>
#include <SFML/System/Vector2.hpp>
#include <vector>
#include <memory>
#include <map>
// Forward declarations
class UIGrid;
//=============================================================================
// AStarPath - A computed A* path result, consumed like an iterator
//=============================================================================
struct PyAStarPathObject {
PyObject_HEAD
std::vector<sf::Vector2i> path; // Pre-computed path positions
size_t current_index; // Next step to return
sf::Vector2i origin; // Fixed at creation
sf::Vector2i destination; // Fixed at creation
};
//=============================================================================
// DijkstraMap - A Dijkstra distance field from a fixed root
//=============================================================================
class DijkstraMap {
public:
DijkstraMap(TCODMap* map, int root_x, int root_y, float diagonal_cost);
~DijkstraMap();
// Non-copyable (owns TCODDijkstra)
DijkstraMap(const DijkstraMap&) = delete;
DijkstraMap& operator=(const DijkstraMap&) = delete;
// Queries
float getDistance(int x, int y) const;
std::vector<sf::Vector2i> getPathFrom(int x, int y) const;
sf::Vector2i stepFrom(int x, int y, bool* valid = nullptr) const;
// Accessors
sf::Vector2i getRoot() const { return root; }
float getDiagonalCost() const { return diagonal_cost; }
private:
TCODDijkstra* tcod_dijkstra; // Owned by this object
TCODMap* tcod_map; // Borrowed from Grid
sf::Vector2i root;
float diagonal_cost;
};
struct PyDijkstraMapObject {
PyObject_HEAD
std::shared_ptr<DijkstraMap> data; // Shared with Grid's collection
};
//=============================================================================
// Helper Functions
//=============================================================================
namespace UIGridPathfinding {
// Extract grid position from Vector, Entity, or tuple
// Sets Python error and returns false on failure
// If expected_grid is provided and obj is Entity, validates grid membership
bool ExtractPosition(PyObject* obj, int* x, int* y,
UIGrid* expected_grid = nullptr,
const char* arg_name = "position");
//=========================================================================
// AStarPath Python Type Methods
//=========================================================================
PyObject* AStarPath_new(PyTypeObject* type, PyObject* args, PyObject* kwds);
int AStarPath_init(PyAStarPathObject* self, PyObject* args, PyObject* kwds);
void AStarPath_dealloc(PyAStarPathObject* self);
PyObject* AStarPath_repr(PyAStarPathObject* self);
// Methods
PyObject* AStarPath_walk(PyAStarPathObject* self, PyObject* args);
PyObject* AStarPath_peek(PyAStarPathObject* self, PyObject* args);
// Properties
PyObject* AStarPath_get_origin(PyAStarPathObject* self, void* closure);
PyObject* AStarPath_get_destination(PyAStarPathObject* self, void* closure);
PyObject* AStarPath_get_remaining(PyAStarPathObject* self, void* closure);
// Sequence protocol
Py_ssize_t AStarPath_len(PyAStarPathObject* self);
int AStarPath_bool(PyObject* self);
PyObject* AStarPath_iter(PyAStarPathObject* self);
PyObject* AStarPath_iternext(PyAStarPathObject* self);
//=========================================================================
// DijkstraMap Python Type Methods
//=========================================================================
PyObject* DijkstraMap_new(PyTypeObject* type, PyObject* args, PyObject* kwds);
int DijkstraMap_init(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
void DijkstraMap_dealloc(PyDijkstraMapObject* self);
PyObject* DijkstraMap_repr(PyDijkstraMapObject* self);
// Methods
PyObject* DijkstraMap_distance(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
PyObject* DijkstraMap_path_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
PyObject* DijkstraMap_step_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
// Properties
PyObject* DijkstraMap_get_root(PyDijkstraMapObject* self, void* closure);
//=========================================================================
// Grid Factory Methods (called from UIGrid Python bindings)
//=========================================================================
// Grid.find_path() -> AStarPath | None
PyObject* Grid_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
// Grid.get_dijkstra_map() -> DijkstraMap
PyObject* Grid_get_dijkstra_map(PyUIGridObject* self, PyObject* args, PyObject* kwds);
// Grid.clear_dijkstra_maps() -> None
PyObject* Grid_clear_dijkstra_maps(PyUIGridObject* self, PyObject* args);
}
//=============================================================================
// Python Type Definitions
//=============================================================================
namespace mcrfpydef {
// AStarPath iterator type
struct PyAStarPathIterObject {
PyObject_HEAD
PyAStarPathObject* path; // Reference to path being iterated
size_t iter_index; // Current iteration position
};
extern PyNumberMethods PyAStarPath_as_number;
extern PySequenceMethods PyAStarPath_as_sequence;
extern PyMethodDef PyAStarPath_methods[];
extern PyGetSetDef PyAStarPath_getsetters[];
extern PyTypeObject PyAStarPathType;
extern PyTypeObject PyAStarPathIterType;
extern PyMethodDef PyDijkstraMap_methods[];
extern PyGetSetDef PyDijkstraMap_getsetters[];
extern PyTypeObject PyDijkstraMapType;
}