Compare commits
No commits in common. "87444c2fd07993c43ee9b5cedd94f3c304a05029" and "9eacedc624b26b40063fd6259fb71da3fbe948f5" have entirely different histories.
87444c2fd0
...
9eacedc624
12 changed files with 510 additions and 2650 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -2,10 +2,6 @@
|
||||||
#include <SFML/Graphics.hpp>
|
#include <SFML/Graphics.hpp>
|
||||||
#include <SFML/Audio.hpp>
|
#include <SFML/Audio.hpp>
|
||||||
|
|
||||||
// Maximum dimension for grids, layers, and heightmaps (8192x8192 = 256MB of float data)
|
|
||||||
// Prevents integer overflow in size calculations and limits memory allocation
|
|
||||||
constexpr int GRID_MAX = 8192;
|
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,6 @@
|
||||||
#include "PyMusic.h"
|
#include "PyMusic.h"
|
||||||
#include "PyKeyboard.h"
|
#include "PyKeyboard.h"
|
||||||
#include "PyMouse.h"
|
#include "PyMouse.h"
|
||||||
#include "UIGridPathfinding.h" // AStarPath and DijkstraMap types
|
|
||||||
#include "PyHeightMap.h" // Procedural generation heightmap (#193)
|
|
||||||
#include "McRogueFaceVersion.h"
|
#include "McRogueFaceVersion.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "ImGuiConsole.h"
|
#include "ImGuiConsole.h"
|
||||||
|
|
@ -412,13 +410,6 @@ PyObject* PyInit_mcrfpy()
|
||||||
/*mouse state (#186)*/
|
/*mouse state (#186)*/
|
||||||
&PyMouseType,
|
&PyMouseType,
|
||||||
|
|
||||||
/*pathfinding result types*/
|
|
||||||
&mcrfpydef::PyAStarPathType,
|
|
||||||
&mcrfpydef::PyDijkstraMapType,
|
|
||||||
|
|
||||||
/*procedural generation (#192)*/
|
|
||||||
&mcrfpydef::PyHeightMapType,
|
|
||||||
|
|
||||||
nullptr};
|
nullptr};
|
||||||
|
|
||||||
// Types that are used internally but NOT exported to module namespace (#189)
|
// Types that are used internally but NOT exported to module namespace (#189)
|
||||||
|
|
@ -431,9 +422,6 @@ PyObject* PyInit_mcrfpy()
|
||||||
&PyUICollectionType, &PyUICollectionIterType,
|
&PyUICollectionType, &PyUICollectionIterType,
|
||||||
&PyUIEntityCollectionType, &PyUIEntityCollectionIterType,
|
&PyUIEntityCollectionType, &PyUIEntityCollectionIterType,
|
||||||
|
|
||||||
/*pathfinding iterator - returned by AStarPath.__iter__() but not directly instantiable*/
|
|
||||||
&mcrfpydef::PyAStarPathIterType,
|
|
||||||
|
|
||||||
nullptr};
|
nullptr};
|
||||||
|
|
||||||
// Set up PyWindowType methods and getsetters before PyType_Ready
|
// Set up PyWindowType methods and getsetters before PyType_Ready
|
||||||
|
|
@ -444,10 +432,6 @@ PyObject* PyInit_mcrfpy()
|
||||||
PySceneType.tp_methods = PySceneClass::methods;
|
PySceneType.tp_methods = PySceneClass::methods;
|
||||||
PySceneType.tp_getset = PySceneClass::getsetters;
|
PySceneType.tp_getset = PySceneClass::getsetters;
|
||||||
|
|
||||||
// Set up PyHeightMapType methods and getsetters (#193)
|
|
||||||
mcrfpydef::PyHeightMapType.tp_methods = PyHeightMap::methods;
|
|
||||||
mcrfpydef::PyHeightMapType.tp_getset = PyHeightMap::getsetters;
|
|
||||||
|
|
||||||
// Set up weakref support for all types that need it
|
// Set up weakref support for all types that need it
|
||||||
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
|
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
|
||||||
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
|
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,32 @@ static PyObject* McRFPy_Libtcod::compute_fov(PyObject* self, PyObject* args) {
|
||||||
return visible_list;
|
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
|
// Line drawing algorithm
|
||||||
static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) {
|
static PyObject* McRFPy_Libtcod::line(PyObject* self, PyObject* args) {
|
||||||
int x1, y1, x2, y2;
|
int x1, y1, x2, y2;
|
||||||
|
|
@ -86,8 +112,80 @@ static PyObject* McRFPy_Libtcod::line_iter(PyObject* self, PyObject* args) {
|
||||||
return line(self, args);
|
return line(self, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pathfinding functions removed - use Grid.find_path() and Grid.get_dijkstra_map() instead
|
// Dijkstra pathfinding
|
||||||
// These return AStarPath and DijkstraMap objects (see UIGridPathfinding.h)
|
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)
|
||||||
|
|
||||||
// Method definitions
|
// Method definitions
|
||||||
static PyMethodDef libtcodMethods[] = {
|
static PyMethodDef libtcodMethods[] = {
|
||||||
|
|
@ -103,6 +201,17 @@ static PyMethodDef libtcodMethods[] = {
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" List of (x, y) tuples for visible cells"},
|
" 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", McRFPy_Libtcod::line, METH_VARARGS,
|
||||||
"line(x1, y1, x2, y2)\n\n"
|
"line(x1, y1, x2, y2)\n\n"
|
||||||
"Get cells along a line using Bresenham's algorithm.\n\n"
|
"Get cells along a line using Bresenham's algorithm.\n\n"
|
||||||
|
|
@ -121,6 +230,40 @@ static PyMethodDef libtcodMethods[] = {
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" Iterator of (x, y) tuples along the line"},
|
" 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}
|
{NULL, NULL, 0, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -128,7 +271,7 @@ static PyMethodDef libtcodMethods[] = {
|
||||||
static PyModuleDef libtcodModule = {
|
static PyModuleDef libtcodModule = {
|
||||||
PyModuleDef_HEAD_INIT,
|
PyModuleDef_HEAD_INIT,
|
||||||
"mcrfpy.libtcod",
|
"mcrfpy.libtcod",
|
||||||
"TCOD-compatible algorithms for field of view and line drawing.\n\n"
|
"TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n"
|
||||||
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\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"
|
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
|
||||||
"FOV Algorithms (use mcrfpy.FOV enum):\n"
|
"FOV Algorithms (use mcrfpy.FOV enum):\n"
|
||||||
|
|
@ -138,15 +281,12 @@ static PyModuleDef libtcodModule = {
|
||||||
" mcrfpy.FOV.PERMISSIVE_0 through PERMISSIVE_8 - Permissive variants\n"
|
" mcrfpy.FOV.PERMISSIVE_0 through PERMISSIVE_8 - Permissive variants\n"
|
||||||
" mcrfpy.FOV.RESTRICTIVE - Most restrictive FOV\n"
|
" mcrfpy.FOV.RESTRICTIVE - Most restrictive FOV\n"
|
||||||
" mcrfpy.FOV.SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\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"
|
"Example:\n"
|
||||||
" import mcrfpy\n"
|
" import mcrfpy\n"
|
||||||
" from mcrfpy import libtcod\n\n"
|
" from mcrfpy import libtcod\n\n"
|
||||||
" grid = mcrfpy.Grid(50, 50)\n"
|
" grid = mcrfpy.Grid(50, 50)\n"
|
||||||
" visible = libtcod.compute_fov(grid, 25, 25, 10)\n"
|
" visible = libtcod.compute_fov(grid, 25, 25, 10)\n"
|
||||||
" path = grid.find_path((0, 0), (49, 49)) # Returns AStarPath",
|
" path = libtcod.find_path(grid, 0, 0, 49, 49)",
|
||||||
-1,
|
-1,
|
||||||
libtcodMethods
|
libtcodMethods
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,302 +0,0 @@
|
||||||
#include "PyHeightMap.h"
|
|
||||||
#include "McRFPy_API.h"
|
|
||||||
#include "McRFPy_Doc.h"
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
// Property definitions
|
|
||||||
PyGetSetDef PyHeightMap::getsetters[] = {
|
|
||||||
{"size", (getter)PyHeightMap::get_size, NULL,
|
|
||||||
MCRF_PROPERTY(size, "Dimensions (width, height) of the heightmap. Read-only."), NULL},
|
|
||||||
{NULL}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Method definitions
|
|
||||||
PyMethodDef PyHeightMap::methods[] = {
|
|
||||||
{"fill", (PyCFunction)PyHeightMap::fill, METH_VARARGS,
|
|
||||||
MCRF_METHOD(HeightMap, fill,
|
|
||||||
MCRF_SIG("(value: float)", "HeightMap"),
|
|
||||||
MCRF_DESC("Set all cells to the specified value."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("value", "The value to set for all cells")
|
|
||||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
||||||
)},
|
|
||||||
{"clear", (PyCFunction)PyHeightMap::clear, METH_NOARGS,
|
|
||||||
MCRF_METHOD(HeightMap, clear,
|
|
||||||
MCRF_SIG("()", "HeightMap"),
|
|
||||||
MCRF_DESC("Set all cells to 0.0. Equivalent to fill(0.0)."),
|
|
||||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
||||||
)},
|
|
||||||
{"add_constant", (PyCFunction)PyHeightMap::add_constant, METH_VARARGS,
|
|
||||||
MCRF_METHOD(HeightMap, add_constant,
|
|
||||||
MCRF_SIG("(value: float)", "HeightMap"),
|
|
||||||
MCRF_DESC("Add a constant value to every cell."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("value", "The value to add to each cell")
|
|
||||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
||||||
)},
|
|
||||||
{"scale", (PyCFunction)PyHeightMap::scale, METH_VARARGS,
|
|
||||||
MCRF_METHOD(HeightMap, scale,
|
|
||||||
MCRF_SIG("(factor: float)", "HeightMap"),
|
|
||||||
MCRF_DESC("Multiply every cell by a factor."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("factor", "The multiplier for each cell")
|
|
||||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
||||||
)},
|
|
||||||
{"clamp", (PyCFunction)PyHeightMap::clamp, METH_VARARGS | METH_KEYWORDS,
|
|
||||||
MCRF_METHOD(HeightMap, clamp,
|
|
||||||
MCRF_SIG("(min: float = 0.0, max: float = 1.0)", "HeightMap"),
|
|
||||||
MCRF_DESC("Clamp all values to the specified range."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("min", "Minimum value (default 0.0)")
|
|
||||||
MCRF_ARG("max", "Maximum value (default 1.0)")
|
|
||||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
||||||
)},
|
|
||||||
{"normalize", (PyCFunction)PyHeightMap::normalize, METH_VARARGS | METH_KEYWORDS,
|
|
||||||
MCRF_METHOD(HeightMap, normalize,
|
|
||||||
MCRF_SIG("(min: float = 0.0, max: float = 1.0)", "HeightMap"),
|
|
||||||
MCRF_DESC("Linearly rescale values so the current minimum becomes min and current maximum becomes max."),
|
|
||||||
MCRF_ARGS_START
|
|
||||||
MCRF_ARG("min", "Target minimum value (default 0.0)")
|
|
||||||
MCRF_ARG("max", "Target maximum value (default 1.0)")
|
|
||||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
|
||||||
)},
|
|
||||||
{NULL}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
PyObject* PyHeightMap::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
|
||||||
{
|
|
||||||
PyHeightMapObject* self = (PyHeightMapObject*)type->tp_alloc(type, 0);
|
|
||||||
if (self) {
|
|
||||||
self->heightmap = nullptr;
|
|
||||||
}
|
|
||||||
return (PyObject*)self;
|
|
||||||
}
|
|
||||||
|
|
||||||
int PyHeightMap::init(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
|
||||||
{
|
|
||||||
static const char* keywords[] = {"size", "fill", nullptr};
|
|
||||||
PyObject* size_obj = nullptr;
|
|
||||||
float fill_value = 0.0f;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(keywords),
|
|
||||||
&size_obj, &fill_value)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse size tuple
|
|
||||||
if (!PyTuple_Check(size_obj) || PyTuple_Size(size_obj) != 2) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple of (width, height)");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int width = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 0));
|
|
||||||
int height = (int)PyLong_AsLong(PyTuple_GetItem(size_obj, 1));
|
|
||||||
|
|
||||||
if (PyErr_Occurred()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width <= 0 || height <= 0) {
|
|
||||||
PyErr_SetString(PyExc_ValueError, "width and height must be positive integers");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width > GRID_MAX || height > GRID_MAX) {
|
|
||||||
PyErr_Format(PyExc_ValueError,
|
|
||||||
"HeightMap dimensions cannot exceed %d (got %dx%d)",
|
|
||||||
GRID_MAX, width, height);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up any existing heightmap
|
|
||||||
if (self->heightmap) {
|
|
||||||
TCOD_heightmap_delete(self->heightmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new libtcod heightmap
|
|
||||||
self->heightmap = TCOD_heightmap_new(width, height);
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_MemoryError, "Failed to allocate heightmap");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill with initial value if not zero
|
|
||||||
if (fill_value != 0.0f) {
|
|
||||||
// libtcod's TCOD_heightmap_add adds to all cells, so we use it after clear
|
|
||||||
TCOD_heightmap_clear(self->heightmap);
|
|
||||||
TCOD_heightmap_add(self->heightmap, fill_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PyHeightMap::dealloc(PyHeightMapObject* self)
|
|
||||||
{
|
|
||||||
if (self->heightmap) {
|
|
||||||
TCOD_heightmap_delete(self->heightmap);
|
|
||||||
self->heightmap = nullptr;
|
|
||||||
}
|
|
||||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject* PyHeightMap::repr(PyObject* obj)
|
|
||||||
{
|
|
||||||
PyHeightMapObject* self = (PyHeightMapObject*)obj;
|
|
||||||
std::ostringstream ss;
|
|
||||||
|
|
||||||
if (self->heightmap) {
|
|
||||||
ss << "<HeightMap (" << self->heightmap->w << " x " << self->heightmap->h << ")>";
|
|
||||||
} else {
|
|
||||||
ss << "<HeightMap (uninitialized)>";
|
|
||||||
}
|
|
||||||
|
|
||||||
return PyUnicode_FromString(ss.str().c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Property: size
|
|
||||||
PyObject* PyHeightMap::get_size(PyHeightMapObject* self, void* closure)
|
|
||||||
{
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return Py_BuildValue("(ii)", self->heightmap->w, self->heightmap->h);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: fill(value) -> HeightMap
|
|
||||||
PyObject* PyHeightMap::fill(PyHeightMapObject* self, PyObject* args)
|
|
||||||
{
|
|
||||||
float value;
|
|
||||||
if (!PyArg_ParseTuple(args, "f", &value)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear and then add the value (libtcod doesn't have a direct "set all" function)
|
|
||||||
TCOD_heightmap_clear(self->heightmap);
|
|
||||||
if (value != 0.0f) {
|
|
||||||
TCOD_heightmap_add(self->heightmap, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return self for chaining
|
|
||||||
Py_INCREF(self);
|
|
||||||
return (PyObject*)self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: clear() -> HeightMap
|
|
||||||
PyObject* PyHeightMap::clear(PyHeightMapObject* self, PyObject* Py_UNUSED(args))
|
|
||||||
{
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
TCOD_heightmap_clear(self->heightmap);
|
|
||||||
|
|
||||||
// Return self for chaining
|
|
||||||
Py_INCREF(self);
|
|
||||||
return (PyObject*)self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: add_constant(value) -> HeightMap
|
|
||||||
PyObject* PyHeightMap::add_constant(PyHeightMapObject* self, PyObject* args)
|
|
||||||
{
|
|
||||||
float value;
|
|
||||||
if (!PyArg_ParseTuple(args, "f", &value)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
TCOD_heightmap_add(self->heightmap, value);
|
|
||||||
|
|
||||||
// Return self for chaining
|
|
||||||
Py_INCREF(self);
|
|
||||||
return (PyObject*)self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: scale(factor) -> HeightMap
|
|
||||||
PyObject* PyHeightMap::scale(PyHeightMapObject* self, PyObject* args)
|
|
||||||
{
|
|
||||||
float factor;
|
|
||||||
if (!PyArg_ParseTuple(args, "f", &factor)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
TCOD_heightmap_scale(self->heightmap, factor);
|
|
||||||
|
|
||||||
// Return self for chaining
|
|
||||||
Py_INCREF(self);
|
|
||||||
return (PyObject*)self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: clamp(min=0.0, max=1.0) -> HeightMap
|
|
||||||
PyObject* PyHeightMap::clamp(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
|
||||||
{
|
|
||||||
static const char* keywords[] = {"min", "max", nullptr};
|
|
||||||
float min_val = 0.0f;
|
|
||||||
float max_val = 1.0f;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ff", const_cast<char**>(keywords),
|
|
||||||
&min_val, &max_val)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (min_val > max_val) {
|
|
||||||
PyErr_SetString(PyExc_ValueError, "min must be less than or equal to max");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
TCOD_heightmap_clamp(self->heightmap, min_val, max_val);
|
|
||||||
|
|
||||||
// Return self for chaining
|
|
||||||
Py_INCREF(self);
|
|
||||||
return (PyObject*)self;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method: normalize(min=0.0, max=1.0) -> HeightMap
|
|
||||||
PyObject* PyHeightMap::normalize(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
|
||||||
{
|
|
||||||
static const char* keywords[] = {"min", "max", nullptr};
|
|
||||||
float min_val = 0.0f;
|
|
||||||
float max_val = 1.0f;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ff", const_cast<char**>(keywords),
|
|
||||||
&min_val, &max_val)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self->heightmap) {
|
|
||||||
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (min_val > max_val) {
|
|
||||||
PyErr_SetString(PyExc_ValueError, "min must be less than or equal to max");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
TCOD_heightmap_normalize(self->heightmap, min_val, max_val);
|
|
||||||
|
|
||||||
// Return self for chaining
|
|
||||||
Py_INCREF(self);
|
|
||||||
return (PyObject*)self;
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include "Common.h"
|
|
||||||
#include "Python.h"
|
|
||||||
#include <libtcod.h>
|
|
||||||
|
|
||||||
// Forward declaration
|
|
||||||
class PyHeightMap;
|
|
||||||
|
|
||||||
// Python object structure
|
|
||||||
typedef struct {
|
|
||||||
PyObject_HEAD
|
|
||||||
TCOD_heightmap_t* heightmap; // libtcod heightmap pointer
|
|
||||||
} PyHeightMapObject;
|
|
||||||
|
|
||||||
class PyHeightMap
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
// Python type interface
|
|
||||||
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
|
||||||
static int init(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
|
||||||
static void dealloc(PyHeightMapObject* self);
|
|
||||||
static PyObject* repr(PyObject* obj);
|
|
||||||
|
|
||||||
// Properties
|
|
||||||
static PyObject* get_size(PyHeightMapObject* self, void* closure);
|
|
||||||
|
|
||||||
// Scalar operations (all return self for chaining)
|
|
||||||
static PyObject* fill(PyHeightMapObject* self, PyObject* args);
|
|
||||||
static PyObject* clear(PyHeightMapObject* self, PyObject* Py_UNUSED(args));
|
|
||||||
static PyObject* add_constant(PyHeightMapObject* self, PyObject* args);
|
|
||||||
static PyObject* scale(PyHeightMapObject* self, PyObject* args);
|
|
||||||
static PyObject* clamp(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
|
||||||
static PyObject* normalize(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
|
||||||
|
|
||||||
// Method and property definitions
|
|
||||||
static PyMethodDef methods[];
|
|
||||||
static PyGetSetDef getsetters[];
|
|
||||||
};
|
|
||||||
|
|
||||||
namespace mcrfpydef {
|
|
||||||
static PyTypeObject PyHeightMapType = {
|
|
||||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
|
||||||
.tp_name = "mcrfpy.HeightMap",
|
|
||||||
.tp_basicsize = sizeof(PyHeightMapObject),
|
|
||||||
.tp_itemsize = 0,
|
|
||||||
.tp_dealloc = (destructor)PyHeightMap::dealloc,
|
|
||||||
.tp_repr = PyHeightMap::repr,
|
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
||||||
.tp_doc = PyDoc_STR(
|
|
||||||
"HeightMap(size: tuple[int, int], fill: float = 0.0)\n\n"
|
|
||||||
"A 2D grid of float values for procedural generation.\n\n"
|
|
||||||
"HeightMap is the universal canvas for procedural generation. It stores "
|
|
||||||
"float values that can be manipulated, combined, and applied to Grid and "
|
|
||||||
"Layer objects.\n\n"
|
|
||||||
"Args:\n"
|
|
||||||
" size: (width, height) dimensions of the heightmap. Immutable after creation.\n"
|
|
||||||
" fill: Initial value for all cells. Default 0.0.\n\n"
|
|
||||||
"Example:\n"
|
|
||||||
" hmap = mcrfpy.HeightMap((100, 100))\n"
|
|
||||||
" hmap.fill(0.5).scale(2.0).clamp(0.0, 1.0)\n"
|
|
||||||
),
|
|
||||||
.tp_methods = nullptr, // Set in McRFPy_API.cpp before PyType_Ready
|
|
||||||
.tp_getset = nullptr, // Set in McRFPy_API.cpp before PyType_Ready
|
|
||||||
.tp_init = (initproc)PyHeightMap::init,
|
|
||||||
.tp_new = PyHeightMap::pynew,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <libtcod.h>
|
|
||||||
#include "PyObjectUtils.h"
|
#include "PyObjectUtils.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
|
|
@ -763,29 +762,23 @@ PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kw
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use A* pathfinding via temporary TCODPath
|
// Use the grid's Dijkstra implementation
|
||||||
TCODPath tcod_path(grid->getTCODMap(), 1.41f);
|
grid->computeDijkstra(current_x, current_y);
|
||||||
if (!tcod_path.compute(current_x, current_y, target_x, target_y)) {
|
auto path = grid->getDijkstraPath(target_x, target_y);
|
||||||
// No path found - return empty list
|
|
||||||
return PyList_New(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert path to Python list of tuples
|
// Convert path to Python list of tuples
|
||||||
PyObject* path_list = PyList_New(tcod_path.size());
|
PyObject* path_list = PyList_New(path.size());
|
||||||
if (!path_list) return PyErr_NoMemory();
|
if (!path_list) return PyErr_NoMemory();
|
||||||
|
|
||||||
for (int i = 0; i < tcod_path.size(); ++i) {
|
for (size_t i = 0; i < path.size(); ++i) {
|
||||||
int px, py;
|
|
||||||
tcod_path.get(i, &px, &py);
|
|
||||||
|
|
||||||
PyObject* coord_tuple = PyTuple_New(2);
|
PyObject* coord_tuple = PyTuple_New(2);
|
||||||
if (!coord_tuple) {
|
if (!coord_tuple) {
|
||||||
Py_DECREF(path_list);
|
Py_DECREF(path_list);
|
||||||
return PyErr_NoMemory();
|
return PyErr_NoMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
PyTuple_SetItem(coord_tuple, 0, PyLong_FromLong(px));
|
PyTuple_SetItem(coord_tuple, 0, PyLong_FromLong(path[i].first));
|
||||||
PyTuple_SetItem(coord_tuple, 1, PyLong_FromLong(py));
|
PyTuple_SetItem(coord_tuple, 1, PyLong_FromLong(path[i].second));
|
||||||
PyList_SetItem(path_list, i, coord_tuple);
|
PyList_SetItem(path_list, i, coord_tuple);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
372
src/UIGrid.cpp
372
src/UIGrid.cpp
|
|
@ -1,5 +1,4 @@
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "UIGridPathfinding.h" // New pathfinding API
|
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
|
|
@ -18,7 +17,7 @@
|
||||||
|
|
||||||
UIGrid::UIGrid()
|
UIGrid::UIGrid()
|
||||||
: grid_w(0), grid_h(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
|
: 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),
|
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||||
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
|
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
|
||||||
use_chunks(false) // Default to omniscient view
|
use_chunks(false) // Default to omniscient view
|
||||||
{
|
{
|
||||||
|
|
@ -50,7 +49,7 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
||||||
: grid_w(gx), grid_h(gy),
|
: grid_w(gx), grid_h(gy),
|
||||||
zoom(1.0f),
|
zoom(1.0f),
|
||||||
ptex(_ptex),
|
ptex(_ptex),
|
||||||
fill_color(8, 8, 8, 255), tcod_map(nullptr),
|
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||||
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
|
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
|
||||||
use_chunks(gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD) // #123 - Use chunks for large grids
|
use_chunks(gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD) // #123 - Use chunks for large grids
|
||||||
{
|
{
|
||||||
|
|
@ -85,10 +84,14 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
||||||
// textures are upside-down inside renderTexture
|
// textures are upside-down inside renderTexture
|
||||||
output.setTexture(renderTexture.getTexture());
|
output.setTexture(renderTexture.getTexture());
|
||||||
|
|
||||||
// Create TCOD map for FOV and as source for pathfinding
|
// Create TCOD map
|
||||||
tcod_map = new TCODMap(gx, gy);
|
tcod_map = new TCODMap(gx, gy);
|
||||||
// Note: DijkstraMap objects are created on-demand via get_dijkstra_map()
|
|
||||||
// A* paths are computed on-demand via find_path()
|
// Create TCOD dijkstra pathfinder
|
||||||
|
tcod_dijkstra = new TCODDijkstra(tcod_map);
|
||||||
|
|
||||||
|
// Create TCOD A* pathfinder
|
||||||
|
tcod_path = new TCODPath(tcod_map);
|
||||||
|
|
||||||
// #123 - Initialize storage based on grid size
|
// #123 - Initialize storage based on grid size
|
||||||
if (use_chunks) {
|
if (use_chunks) {
|
||||||
|
|
@ -365,9 +368,14 @@ UIGridPoint& UIGrid::at(int x, int y)
|
||||||
|
|
||||||
UIGrid::~UIGrid()
|
UIGrid::~UIGrid()
|
||||||
{
|
{
|
||||||
// Clear Dijkstra maps first (they reference tcod_map)
|
if (tcod_path) {
|
||||||
dijkstra_maps.clear();
|
delete tcod_path;
|
||||||
|
tcod_path = nullptr;
|
||||||
|
}
|
||||||
|
if (tcod_dijkstra) {
|
||||||
|
delete tcod_dijkstra;
|
||||||
|
tcod_dijkstra = nullptr;
|
||||||
|
}
|
||||||
if (tcod_map) {
|
if (tcod_map) {
|
||||||
delete tcod_map;
|
delete tcod_map;
|
||||||
tcod_map = nullptr;
|
tcod_map = nullptr;
|
||||||
|
|
@ -468,9 +476,98 @@ bool UIGrid::isInFOV(int x, int y) const
|
||||||
return tcod_map->isInFov(x, y);
|
return tcod_map->isInFov(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pathfinding methods moved to UIGridPathfinding.cpp
|
std::vector<std::pair<int, int>> UIGrid::findPath(int x1, int y1, int x2, int y2, float diagonalCost)
|
||||||
// - Grid.find_path() returns AStarPath objects
|
{
|
||||||
// - Grid.get_dijkstra_map() returns DijkstraMap objects (cached)
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 1 implementations
|
// Phase 1 implementations
|
||||||
sf::FloatRect UIGrid::get_bounds() const
|
sf::FloatRect UIGrid::get_bounds() const
|
||||||
|
|
@ -1356,9 +1453,128 @@ PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* k
|
||||||
return PyBool_FromLong(in_fov);
|
return PyBool_FromLong(in_fov);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Old pathfinding Python methods removed - see UIGridPathfinding.cpp for new implementation
|
PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||||
// Grid.find_path() now returns AStarPath objects
|
{
|
||||||
// Grid.get_dijkstra_map() returns DijkstraMap objects (cached by root)
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// #147 - Layer system Python API
|
// #147 - Layer system Python API
|
||||||
PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
|
@ -1709,32 +1925,51 @@ PyMethodDef UIGrid::methods[] = {
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" True if the cell is visible, False otherwise\n\n"
|
" True if the cell is visible, False otherwise\n\n"
|
||||||
"Must call compute_fov() first to calculate visibility."},
|
"Must call compute_fov() first to calculate visibility."},
|
||||||
{"find_path", (PyCFunction)UIGridPathfinding::Grid_find_path, METH_VARARGS | METH_KEYWORDS,
|
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
|
||||||
"find_path(start, end, diagonal_cost: float = 1.41) -> AStarPath | None\n\n"
|
"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"
|
||||||
"Compute A* path between two points.\n\n"
|
"Compute A* path between two points.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" start: Starting position as Vector, Entity, or (x, y) tuple\n"
|
" start: Starting position as (x, y) tuple, list, or Vector\n"
|
||||||
" end: Target position as Vector, Entity, or (x, y) tuple\n"
|
" end: Target position as (x, y) tuple, list, or Vector\n"
|
||||||
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" AStarPath object if path exists, None otherwise.\n\n"
|
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
|
||||||
"The returned AStarPath can be iterated or walked step-by-step."},
|
"Alternative A* implementation. Prefer find_path() for consistency."},
|
||||||
{"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", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS | METH_KEYWORDS,
|
||||||
"add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer"},
|
"add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer"},
|
||||||
{"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS,
|
{"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS,
|
||||||
|
|
@ -1786,32 +2021,51 @@ PyMethodDef UIGrid_all_methods[] = {
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" True if the cell is visible, False otherwise\n\n"
|
" True if the cell is visible, False otherwise\n\n"
|
||||||
"Must call compute_fov() first to calculate visibility."},
|
"Must call compute_fov() first to calculate visibility."},
|
||||||
{"find_path", (PyCFunction)UIGridPathfinding::Grid_find_path, METH_VARARGS | METH_KEYWORDS,
|
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
|
||||||
"find_path(start, end, diagonal_cost: float = 1.41) -> AStarPath | None\n\n"
|
"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"
|
||||||
"Compute A* path between two points.\n\n"
|
"Compute A* path between two points.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" start: Starting position as Vector, Entity, or (x, y) tuple\n"
|
" start: Starting position as (x, y) tuple, list, or Vector\n"
|
||||||
" end: Target position as Vector, Entity, or (x, y) tuple\n"
|
" end: Target position as (x, y) tuple, list, or Vector\n"
|
||||||
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" AStarPath object if path exists, None otherwise.\n\n"
|
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
|
||||||
"The returned AStarPath can be iterated or walked step-by-step."},
|
"Alternative A* implementation. Prefer find_path() for consistency."},
|
||||||
{"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", (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_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer\n\n"
|
||||||
"Add a new layer to the grid.\n\n"
|
"Add a new layer to the grid.\n\n"
|
||||||
|
|
|
||||||
34
src/UIGrid.h
34
src/UIGrid.h
|
|
@ -8,8 +8,6 @@
|
||||||
#include <libtcod.h>
|
#include <libtcod.h>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include "PyCallable.h"
|
#include "PyCallable.h"
|
||||||
#include "PyTexture.h"
|
#include "PyTexture.h"
|
||||||
|
|
@ -27,9 +25,6 @@
|
||||||
#include "SpatialHash.h"
|
#include "SpatialHash.h"
|
||||||
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
|
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
|
||||||
|
|
||||||
// Forward declaration for pathfinding
|
|
||||||
class DijkstraMap;
|
|
||||||
|
|
||||||
class UIGrid: public UIDrawable
|
class UIGrid: public UIDrawable
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
|
@ -38,13 +33,10 @@ private:
|
||||||
static constexpr int DEFAULT_CELL_WIDTH = 16;
|
static constexpr int DEFAULT_CELL_WIDTH = 16;
|
||||||
static constexpr int DEFAULT_CELL_HEIGHT = 16;
|
static constexpr int DEFAULT_CELL_HEIGHT = 16;
|
||||||
TCODMap* tcod_map; // TCOD map for FOV and pathfinding
|
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
|
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:
|
public:
|
||||||
UIGrid();
|
UIGrid();
|
||||||
//UIGrid(int, int, IndexTexture*, float, float, float, float);
|
//UIGrid(int, int, IndexTexture*, float, float, float, float);
|
||||||
|
|
@ -62,12 +54,15 @@ public:
|
||||||
void syncTCODMapCell(int x, int y); // Sync a single cell to TCOD map
|
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);
|
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;
|
bool isInFOV(int x, int y) const;
|
||||||
TCODMap* getTCODMap() const { return tcod_map; } // Access for pathfinding
|
|
||||||
|
|
||||||
// Pathfinding - new API creates AStarPath/DijkstraMap objects
|
// Pathfinding methods
|
||||||
// See UIGridPathfinding.h for the new pathfinding API
|
std::vector<std::pair<int, int>> findPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f);
|
||||||
// Grid.find_path() now returns AStarPath objects
|
void computeDijkstra(int rootX, int rootY, float diagonalCost = 1.41f);
|
||||||
// Grid.get_dijkstra_map() returns DijkstraMap objects (cached by root position)
|
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);
|
||||||
|
|
||||||
// Phase 1 virtual method implementations
|
// Phase 1 virtual method implementations
|
||||||
sf::FloatRect get_bounds() const override;
|
sf::FloatRect get_bounds() const override;
|
||||||
|
|
@ -172,10 +167,11 @@ public:
|
||||||
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_compute_fov(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_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
// Pathfinding methods moved to UIGridPathfinding.cpp
|
static PyObject* py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
// py_find_path -> UIGridPathfinding::Grid_find_path (returns AStarPath)
|
static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
// py_get_dijkstra_map -> UIGridPathfinding::Grid_get_dijkstra_map (returns DijkstraMap)
|
static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
// py_clear_dijkstra_maps -> UIGridPathfinding::Grid_clear_dijkstra_maps
|
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
|
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
|
||||||
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169
|
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,688 +0,0 @@
|
||||||
#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
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,268 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""Unit tests for mcrfpy.HeightMap core functionality (#193)
|
|
||||||
|
|
||||||
Tests the HeightMap class constructor, size property, and scalar operations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import mcrfpy
|
|
||||||
|
|
||||||
|
|
||||||
def test_constructor_basic():
|
|
||||||
"""HeightMap can be created with a size tuple"""
|
|
||||||
hmap = mcrfpy.HeightMap((100, 50))
|
|
||||||
assert hmap is not None
|
|
||||||
print("PASS: test_constructor_basic")
|
|
||||||
|
|
||||||
|
|
||||||
def test_constructor_with_fill():
|
|
||||||
"""HeightMap can be created with a fill value"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
assert hmap is not None
|
|
||||||
print("PASS: test_constructor_with_fill")
|
|
||||||
|
|
||||||
|
|
||||||
def test_size_property():
|
|
||||||
"""size property returns correct dimensions"""
|
|
||||||
hmap = mcrfpy.HeightMap((100, 50))
|
|
||||||
size = hmap.size
|
|
||||||
assert size == (100, 50), f"Expected (100, 50), got {size}"
|
|
||||||
print("PASS: test_size_property")
|
|
||||||
|
|
||||||
|
|
||||||
def test_size_immutable():
|
|
||||||
"""size property is read-only"""
|
|
||||||
hmap = mcrfpy.HeightMap((100, 50))
|
|
||||||
try:
|
|
||||||
hmap.size = (200, 100)
|
|
||||||
print("FAIL: test_size_immutable - should have raised AttributeError")
|
|
||||||
sys.exit(1)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
print("PASS: test_size_immutable")
|
|
||||||
|
|
||||||
|
|
||||||
def test_fill_method():
|
|
||||||
"""fill() sets all cells and returns self"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
result = hmap.fill(0.5)
|
|
||||||
assert result is hmap, "fill() should return self"
|
|
||||||
print("PASS: test_fill_method")
|
|
||||||
|
|
||||||
|
|
||||||
def test_clear_method():
|
|
||||||
"""clear() sets all cells to 0.0 and returns self"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
result = hmap.clear()
|
|
||||||
assert result is hmap, "clear() should return self"
|
|
||||||
print("PASS: test_clear_method")
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_constant_method():
|
|
||||||
"""add_constant() adds to all cells and returns self"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
result = hmap.add_constant(0.25)
|
|
||||||
assert result is hmap, "add_constant() should return self"
|
|
||||||
print("PASS: test_add_constant_method")
|
|
||||||
|
|
||||||
|
|
||||||
def test_scale_method():
|
|
||||||
"""scale() multiplies all cells and returns self"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
result = hmap.scale(2.0)
|
|
||||||
assert result is hmap, "scale() should return self"
|
|
||||||
print("PASS: test_scale_method")
|
|
||||||
|
|
||||||
|
|
||||||
def test_clamp_method():
|
|
||||||
"""clamp() clamps values and returns self"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
result = hmap.clamp(0.0, 1.0)
|
|
||||||
assert result is hmap, "clamp() should return self"
|
|
||||||
print("PASS: test_clamp_method")
|
|
||||||
|
|
||||||
|
|
||||||
def test_clamp_with_defaults():
|
|
||||||
"""clamp() works with default parameters"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
result = hmap.clamp() # Uses defaults 0.0, 1.0
|
|
||||||
assert result is hmap
|
|
||||||
print("PASS: test_clamp_with_defaults")
|
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_method():
|
|
||||||
"""normalize() rescales values and returns self"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
hmap.fill(0.25).add_constant(0.1) # Some values
|
|
||||||
result = hmap.normalize(0.0, 1.0)
|
|
||||||
assert result is hmap, "normalize() should return self"
|
|
||||||
print("PASS: test_normalize_method")
|
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_with_defaults():
|
|
||||||
"""normalize() works with default parameters"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
result = hmap.normalize() # Uses defaults 0.0, 1.0
|
|
||||||
assert result is hmap
|
|
||||||
print("PASS: test_normalize_with_defaults")
|
|
||||||
|
|
||||||
|
|
||||||
def test_method_chaining():
|
|
||||||
"""Methods can be chained"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10))
|
|
||||||
result = hmap.fill(0.5).scale(2.0).clamp(0.0, 1.0)
|
|
||||||
assert result is hmap, "Chained methods should return self"
|
|
||||||
print("PASS: test_method_chaining")
|
|
||||||
|
|
||||||
|
|
||||||
def test_complex_chaining():
|
|
||||||
"""Complex chains work correctly"""
|
|
||||||
hmap = mcrfpy.HeightMap((100, 100))
|
|
||||||
result = (hmap
|
|
||||||
.fill(0.0)
|
|
||||||
.add_constant(0.5)
|
|
||||||
.scale(1.5)
|
|
||||||
.clamp(0.0, 1.0)
|
|
||||||
.normalize(0.2, 0.8))
|
|
||||||
assert result is hmap
|
|
||||||
print("PASS: test_complex_chaining")
|
|
||||||
|
|
||||||
|
|
||||||
def test_repr():
|
|
||||||
"""repr() returns a readable string"""
|
|
||||||
hmap = mcrfpy.HeightMap((100, 50))
|
|
||||||
r = repr(hmap)
|
|
||||||
assert "HeightMap" in r
|
|
||||||
assert "100" in r and "50" in r
|
|
||||||
print(f"PASS: test_repr - {r}")
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_size():
|
|
||||||
"""Negative or zero size raises ValueError"""
|
|
||||||
try:
|
|
||||||
mcrfpy.HeightMap((0, 10))
|
|
||||||
print("FAIL: test_invalid_size - should have raised ValueError for width=0")
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
mcrfpy.HeightMap((10, -5))
|
|
||||||
print("FAIL: test_invalid_size - should have raised ValueError for height=-5")
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("PASS: test_invalid_size")
|
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_size_type():
|
|
||||||
"""Non-tuple size raises TypeError"""
|
|
||||||
try:
|
|
||||||
mcrfpy.HeightMap([100, 50]) # list instead of tuple
|
|
||||||
print("FAIL: test_invalid_size_type - should have raised TypeError")
|
|
||||||
sys.exit(1)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
print("PASS: test_invalid_size_type")
|
|
||||||
|
|
||||||
|
|
||||||
def test_size_exceeds_grid_max():
|
|
||||||
"""Size exceeding GRID_MAX (8192) raises ValueError"""
|
|
||||||
# Test width exceeds limit
|
|
||||||
try:
|
|
||||||
mcrfpy.HeightMap((10000, 100))
|
|
||||||
print("FAIL: test_size_exceeds_grid_max - should have raised ValueError for width=10000")
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError as e:
|
|
||||||
assert "8192" in str(e) or "cannot exceed" in str(e).lower()
|
|
||||||
|
|
||||||
# Test height exceeds limit
|
|
||||||
try:
|
|
||||||
mcrfpy.HeightMap((100, 10000))
|
|
||||||
print("FAIL: test_size_exceeds_grid_max - should have raised ValueError for height=10000")
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError as e:
|
|
||||||
assert "8192" in str(e) or "cannot exceed" in str(e).lower()
|
|
||||||
|
|
||||||
# Test both exceed limit (would cause integer overflow without validation)
|
|
||||||
try:
|
|
||||||
mcrfpy.HeightMap((65536, 65536))
|
|
||||||
print("FAIL: test_size_exceeds_grid_max - should have raised ValueError for 65536x65536")
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print("PASS: test_size_exceeds_grid_max")
|
|
||||||
|
|
||||||
|
|
||||||
def test_clamp_min_greater_than_max():
|
|
||||||
"""clamp() with min > max raises ValueError"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
try:
|
|
||||||
hmap.clamp(min=1.0, max=0.0)
|
|
||||||
print("FAIL: test_clamp_min_greater_than_max - should have raised ValueError")
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError as e:
|
|
||||||
assert "min" in str(e).lower() and "max" in str(e).lower()
|
|
||||||
print("PASS: test_clamp_min_greater_than_max")
|
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_min_greater_than_max():
|
|
||||||
"""normalize() with min > max raises ValueError"""
|
|
||||||
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
|
|
||||||
try:
|
|
||||||
hmap.normalize(min=1.0, max=0.0)
|
|
||||||
print("FAIL: test_normalize_min_greater_than_max - should have raised ValueError")
|
|
||||||
sys.exit(1)
|
|
||||||
except ValueError as e:
|
|
||||||
assert "min" in str(e).lower() and "max" in str(e).lower()
|
|
||||||
print("PASS: test_normalize_min_greater_than_max")
|
|
||||||
|
|
||||||
|
|
||||||
def test_max_valid_size():
|
|
||||||
"""Size at GRID_MAX boundary works"""
|
|
||||||
# Test at the exact limit - this should work
|
|
||||||
hmap = mcrfpy.HeightMap((8192, 1))
|
|
||||||
assert hmap.size == (8192, 1)
|
|
||||||
|
|
||||||
hmap2 = mcrfpy.HeightMap((1, 8192))
|
|
||||||
assert hmap2.size == (1, 8192)
|
|
||||||
|
|
||||||
print("PASS: test_max_valid_size")
|
|
||||||
|
|
||||||
|
|
||||||
def run_all_tests():
|
|
||||||
"""Run all tests"""
|
|
||||||
print("Running HeightMap basic tests...")
|
|
||||||
print()
|
|
||||||
|
|
||||||
test_constructor_basic()
|
|
||||||
test_constructor_with_fill()
|
|
||||||
test_size_property()
|
|
||||||
test_size_immutable()
|
|
||||||
test_fill_method()
|
|
||||||
test_clear_method()
|
|
||||||
test_add_constant_method()
|
|
||||||
test_scale_method()
|
|
||||||
test_clamp_method()
|
|
||||||
test_clamp_with_defaults()
|
|
||||||
test_normalize_method()
|
|
||||||
test_normalize_with_defaults()
|
|
||||||
test_method_chaining()
|
|
||||||
test_complex_chaining()
|
|
||||||
test_repr()
|
|
||||||
test_invalid_size()
|
|
||||||
test_invalid_size_type()
|
|
||||||
test_size_exceeds_grid_max()
|
|
||||||
test_clamp_min_greater_than_max()
|
|
||||||
test_normalize_min_greater_than_max()
|
|
||||||
test_max_valid_size()
|
|
||||||
|
|
||||||
print()
|
|
||||||
print("All HeightMap basic tests PASSED!")
|
|
||||||
|
|
||||||
|
|
||||||
# Run tests directly
|
|
||||||
run_all_tests()
|
|
||||||
sys.exit(0)
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue