diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index e0dc9de..4eee80a 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -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 diff --git a/src/McRFPy_Libtcod.cpp b/src/McRFPy_Libtcod.cpp index f94d0a9..71b20df 100644 --- a/src/McRFPy_Libtcod.cpp +++ b/src/McRFPy_Libtcod.cpp @@ -10,13 +10,13 @@ static UIGrid* get_grid_from_pyobject(PyObject* obj) { PyErr_SetString(PyExc_RuntimeError, "Could not find Grid type"); return nullptr; } - + if (!PyObject_IsInstance(obj, (PyObject*)grid_type)) { Py_DECREF(grid_type); PyErr_SetString(PyExc_TypeError, "First argument must be a Grid object"); return nullptr; } - + Py_DECREF(grid_type); PyUIGridObject* pygrid = (PyUIGridObject*)obj; return pygrid->data.get(); @@ -28,18 +28,18 @@ static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) { int x, y, radius; int light_walls = 1; int algorithm = FOV_BASIC; - - if (!PyArg_ParseTuple(args, "Oiii|ii", &grid_obj, &x, &y, &radius, + + if (!PyArg_ParseTuple(args, "Oiii|ii", &grid_obj, &x, &y, &radius, &light_walls, &algorithm)) { return NULL; } - + UIGrid* grid = get_grid_from_pyobject(grid_obj); if (!grid) return NULL; - + // Compute FOV using grid's method grid->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm); - + // Return list of visible cells PyObject* visible_list = PyList_New(0); for (int gy = 0; gy < grid->grid_h; gy++) { @@ -51,57 +51,31 @@ 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> 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; + return visible_list; } // Line drawing algorithm static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) { int x1, y1, x2, y2; - + if (!PyArg_ParseTuple(args, "iiii", &x1, &y1, &x2, &y2)) { return NULL; } - + // Use TCOD's line algorithm TCODLine::init(x1, y1, x2, y2); - + PyObject* line_list = PyList_New(0); int x, y; - + // Step through line while (!TCODLine::step(&x, &y)) { PyObject* pos = Py_BuildValue("(ii)", x, y); PyList_Append(line_list, pos); Py_DECREF(pos); } - + return line_list; } @@ -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> 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[] = { @@ -200,18 +102,7 @@ static PyMethodDef libtcodMethods[] = { " algorithm: FOV algorithm (mcrfpy.FOV.BASIC, mcrfpy.FOV.SHADOW, etc.)\n\n" "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" @@ -220,7 +111,7 @@ static PyMethodDef libtcodMethods[] = { " x2, y2: Ending position\n\n" "Returns:\n" " List of (x, y) tuples along the line"}, - + {"line_iter", McRFPy_Libtcod::line_iter, METH_VARARGS, "line_iter(x1, y1, x2, y2)\n\n" "Iterate over cells along a line.\n\n" @@ -229,41 +120,7 @@ static PyMethodDef libtcodMethods[] = { " x2, y2: Ending position\n\n" "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 }; @@ -297,8 +157,8 @@ PyObject* McRFPy_Libtcod::init_libtcod_module() { if (m == NULL) { return NULL; } - + // FOV algorithm constants now provided by mcrfpy.FOV enum (#114) return m; -} \ No newline at end of file +} diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 7950c90..c1586bf 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -3,6 +3,7 @@ #include "McRFPy_API.h" #include #include +#include #include "PyObjectUtils.h" #include "PyVector.h" #include "PythonObjectCache.h" @@ -749,39 +750,45 @@ PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kw PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to compute paths"); return NULL; } - + // Get current position int current_x = static_cast(self->data->position.x); int current_y = static_cast(self->data->position.y); - + // Validate target position auto grid = self->data->grid; if (target_x < 0 || target_x >= grid->grid_w || target_y < 0 || target_y >= grid->grid_h) { - PyErr_Format(PyExc_ValueError, "Target position (%d, %d) is out of grid bounds (0-%d, 0-%d)", + PyErr_Format(PyExc_ValueError, "Target position (%d, %d) is out of grid bounds (0-%d, 0-%d)", target_x, target_y, grid->grid_w - 1, grid->grid_h - 1); 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); } - + return path_list; } diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 35a254e..cff7649 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -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 _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 _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> UIGrid::findPath(int x1, int y1, int x2, int y2, float diagonalCost) -{ - std::vector> 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> UIGrid::getDijkstraPath(int x, int y) const -{ - std::vector> 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> UIGrid::computeAStarPath(int x1, int y1, int x2, int y2, float diagonalCost) -{ - std::vector> 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(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> 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(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> 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(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> 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" diff --git a/src/UIGrid.h b/src/UIGrid.h index b8a9d2d..3d1a56a 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -8,6 +8,8 @@ #include #include #include +#include +#include #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::shared_ptr> 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> 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> getDijkstraPath(int x, int y) const; - - // A* pathfinding methods - std::vector> 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 diff --git a/src/UIGridPathfinding.cpp b/src/UIGridPathfinding.cpp new file mode 100644 index 0000000..d232262 --- /dev/null +++ b/src/UIGridPathfinding.cpp @@ -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 DijkstraMap::getPathFrom(int x, int y) const { + std::vector 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(entity->data->position.x); + *y = static_cast(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(vec->data.x); + *y = static_cast(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(); // 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("", + 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(pos.x), static_cast(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(pos.x), static_cast(pos.y))).pyObject(); +} + +PyObject* UIGridPathfinding::AStarPath_get_origin(PyAStarPathObject* self, void* closure) { + return PyVector(sf::Vector2f(static_cast(self->origin.x), + static_cast(self->origin.y))).pyObject(); +} + +PyObject* UIGridPathfinding::AStarPath_get_destination(PyAStarPathObject* self, void* closure) { + return PyVector(sf::Vector2f(static_cast(self->destination.x), + static_cast(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(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(pos.x), static_cast(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(); + } + 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(""); + } + sf::Vector2i root = self->data->getRoot(); + return PyUnicode_FromFormat("", 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(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(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 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(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(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(step.x), static_cast(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(root.x), static_cast(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(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(); + 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(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(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( + 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(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 diff --git a/src/UIGridPathfinding.h b/src/UIGridPathfinding.h new file mode 100644 index 0000000..07294d0 --- /dev/null +++ b/src/UIGridPathfinding.h @@ -0,0 +1,152 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include "UIBase.h" // For PyUIGridObject typedef +#include +#include +#include +#include +#include + +// Forward declarations +class UIGrid; + +//============================================================================= +// AStarPath - A computed A* path result, consumed like an iterator +//============================================================================= + +struct PyAStarPathObject { + PyObject_HEAD + std::vector 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 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 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; +}