Compare commits
3 commits
9eacedc624
...
87444c2fd0
| Author | SHA1 | Date | |
|---|---|---|---|
| 87444c2fd0 | |||
| c095be4b73 | |||
| b32f5af28c |
12 changed files with 2650 additions and 510 deletions
1026
docs/PROCEDURAL_GENERATION_SPEC.md
Normal file
1026
docs/PROCEDURAL_GENERATION_SPEC.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,10 @@
|
||||||
#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,6 +20,8 @@
|
||||||
#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"
|
||||||
|
|
@ -410,6 +412,13 @@ 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)
|
||||||
|
|
@ -422,6 +431,9 @@ 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
|
||||||
|
|
@ -432,6 +444,10 @@ 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,32 +55,6 @@ 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;
|
||||||
|
|
@ -112,80 +86,8 @@ static PyObject* McRFPy_Libtcod::line_iter(PyObject* self, PyObject* args) {
|
||||||
return line(self, args);
|
return line(self, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dijkstra pathfinding
|
// Pathfinding functions removed - use Grid.find_path() and Grid.get_dijkstra_map() instead
|
||||||
static PyObject* McRFPy_Libtcod::dijkstra_new(PyObject* self, PyObject* args) {
|
// These return AStarPath and DijkstraMap objects (see UIGridPathfinding.h)
|
||||||
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[] = {
|
||||||
|
|
@ -201,17 +103,6 @@ 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"
|
||||||
|
|
@ -230,40 +121,6 @@ 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}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -271,7 +128,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, pathfinding, and line drawing.\n\n"
|
"TCOD-compatible algorithms for field of view and line drawing.\n\n"
|
||||||
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
|
"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"
|
||||||
|
|
@ -281,12 +138,15 @@ 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 = libtcod.find_path(grid, 0, 0, 49, 49)",
|
" path = grid.find_path((0, 0), (49, 49)) # Returns AStarPath",
|
||||||
-1,
|
-1,
|
||||||
libtcodMethods
|
libtcodMethods
|
||||||
};
|
};
|
||||||
|
|
|
||||||
302
src/PyHeightMap.cpp
Normal file
302
src/PyHeightMap.cpp
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
67
src/PyHeightMap.h
Normal file
67
src/PyHeightMap.h
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#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,6 +3,7 @@
|
||||||
#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"
|
||||||
|
|
@ -762,23 +763,29 @@ PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kw
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the grid's Dijkstra implementation
|
// Use A* pathfinding via temporary TCODPath
|
||||||
grid->computeDijkstra(current_x, current_y);
|
TCODPath tcod_path(grid->getTCODMap(), 1.41f);
|
||||||
auto path = grid->getDijkstraPath(target_x, target_y);
|
if (!tcod_path.compute(current_x, current_y, target_x, target_y)) {
|
||||||
|
// No path found - return empty list
|
||||||
|
return PyList_New(0);
|
||||||
|
}
|
||||||
|
|
||||||
// Convert path to Python list of tuples
|
// Convert path to Python list of tuples
|
||||||
PyObject* path_list = PyList_New(path.size());
|
PyObject* path_list = PyList_New(tcod_path.size());
|
||||||
if (!path_list) return PyErr_NoMemory();
|
if (!path_list) return PyErr_NoMemory();
|
||||||
|
|
||||||
for (size_t i = 0; i < path.size(); ++i) {
|
for (int i = 0; i < tcod_path.size(); ++i) {
|
||||||
|
int px, py;
|
||||||
|
tcod_path.get(i, &px, &py);
|
||||||
|
|
||||||
PyObject* coord_tuple = PyTuple_New(2);
|
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(path[i].first));
|
PyTuple_SetItem(coord_tuple, 0, PyLong_FromLong(px));
|
||||||
PyTuple_SetItem(coord_tuple, 1, PyLong_FromLong(path[i].second));
|
PyTuple_SetItem(coord_tuple, 1, PyLong_FromLong(py));
|
||||||
PyList_SetItem(path_list, i, coord_tuple);
|
PyList_SetItem(path_list, i, coord_tuple);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
372
src/UIGrid.cpp
372
src/UIGrid.cpp
|
|
@ -1,4 +1,5 @@
|
||||||
#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"
|
||||||
|
|
@ -17,7 +18,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), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
fill_color(8, 8, 8, 255), tcod_map(nullptr),
|
||||||
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
|
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
|
||||||
use_chunks(false) // Default to omniscient view
|
use_chunks(false) // Default to omniscient view
|
||||||
{
|
{
|
||||||
|
|
@ -49,7 +50,7 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
||||||
: grid_w(gx), grid_h(gy),
|
: 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), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
fill_color(8, 8, 8, 255), tcod_map(nullptr),
|
||||||
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
|
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
|
||||||
{
|
{
|
||||||
|
|
@ -84,14 +85,10 @@ 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
|
// Create TCOD map for FOV and as source for pathfinding
|
||||||
tcod_map = new TCODMap(gx, gy);
|
tcod_map = new TCODMap(gx, gy);
|
||||||
|
// Note: DijkstraMap objects are created on-demand via get_dijkstra_map()
|
||||||
// Create TCOD dijkstra pathfinder
|
// A* paths are computed on-demand via find_path()
|
||||||
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) {
|
||||||
|
|
@ -368,14 +365,9 @@ UIGridPoint& UIGrid::at(int x, int y)
|
||||||
|
|
||||||
UIGrid::~UIGrid()
|
UIGrid::~UIGrid()
|
||||||
{
|
{
|
||||||
if (tcod_path) {
|
// Clear Dijkstra maps first (they reference tcod_map)
|
||||||
delete tcod_path;
|
dijkstra_maps.clear();
|
||||||
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;
|
||||||
|
|
@ -476,98 +468,9 @@ bool UIGrid::isInFOV(int x, int y) const
|
||||||
return tcod_map->isInFov(x, y);
|
return tcod_map->isInFov(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::pair<int, int>> UIGrid::findPath(int x1, int y1, int x2, int y2, float diagonalCost)
|
// Pathfinding methods moved to UIGridPathfinding.cpp
|
||||||
{
|
// - Grid.find_path() returns AStarPath objects
|
||||||
std::vector<std::pair<int, int>> path;
|
// - Grid.get_dijkstra_map() returns DijkstraMap objects (cached)
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -1453,128 +1356,9 @@ PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* k
|
||||||
return PyBool_FromLong(in_fov);
|
return PyBool_FromLong(in_fov);
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
// Old pathfinding Python methods removed - see UIGridPathfinding.cpp for new implementation
|
||||||
{
|
// Grid.find_path() now returns AStarPath objects
|
||||||
static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL};
|
// Grid.get_dijkstra_map() returns DijkstraMap objects (cached by root)
|
||||||
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) {
|
||||||
|
|
@ -1925,51 +1709,32 @@ 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)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
|
{"find_path", (PyCFunction)UIGridPathfinding::Grid_find_path, METH_VARARGS | METH_KEYWORDS,
|
||||||
"find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
|
"find_path(start, end, diagonal_cost: float = 1.41) -> AStarPath | None\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 (x, y) tuple, list, or Vector\n"
|
" start: Starting position as Vector, Entity, or (x, y) tuple\n"
|
||||||
" end: Target position as (x, y) tuple, list, or Vector\n"
|
" end: Target position as Vector, Entity, or (x, y) tuple\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"
|
||||||
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
|
" AStarPath object if path exists, None otherwise.\n\n"
|
||||||
"Alternative A* implementation. Prefer find_path() for consistency."},
|
"The returned AStarPath can be iterated or walked step-by-step."},
|
||||||
|
{"get_dijkstra_map", (PyCFunction)UIGridPathfinding::Grid_get_dijkstra_map, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"get_dijkstra_map(root, diagonal_cost: float = 1.41) -> DijkstraMap\n\n"
|
||||||
|
"Get or create a Dijkstra distance map for a root position.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" root: Root position as Vector, Entity, or (x, y) tuple\n"
|
||||||
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" DijkstraMap object for querying distances and paths.\n\n"
|
||||||
|
"Grid caches DijkstraMaps by root position. Multiple requests for the\n"
|
||||||
|
"same root return the same cached map. Call clear_dijkstra_maps() after\n"
|
||||||
|
"changing grid walkability to invalidate the cache."},
|
||||||
|
{"clear_dijkstra_maps", (PyCFunction)UIGridPathfinding::Grid_clear_dijkstra_maps, METH_NOARGS,
|
||||||
|
"clear_dijkstra_maps() -> None\n\n"
|
||||||
|
"Clear all cached Dijkstra maps.\n\n"
|
||||||
|
"Call this after modifying grid cell walkability to ensure pathfinding\n"
|
||||||
|
"uses updated walkability data."},
|
||||||
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS | METH_KEYWORDS,
|
{"add_layer", (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,
|
||||||
|
|
@ -2021,51 +1786,32 @@ 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)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
|
{"find_path", (PyCFunction)UIGridPathfinding::Grid_find_path, METH_VARARGS | METH_KEYWORDS,
|
||||||
"find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
|
"find_path(start, end, diagonal_cost: float = 1.41) -> AStarPath | None\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 (x, y) tuple, list, or Vector\n"
|
" start: Starting position as Vector, Entity, or (x, y) tuple\n"
|
||||||
" end: Target position as (x, y) tuple, list, or Vector\n"
|
" end: Target position as Vector, Entity, or (x, y) tuple\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"
|
||||||
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
|
" AStarPath object if path exists, None otherwise.\n\n"
|
||||||
"Alternative A* implementation. Prefer find_path() for consistency."},
|
"The returned AStarPath can be iterated or walked step-by-step."},
|
||||||
|
{"get_dijkstra_map", (PyCFunction)UIGridPathfinding::Grid_get_dijkstra_map, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"get_dijkstra_map(root, diagonal_cost: float = 1.41) -> DijkstraMap\n\n"
|
||||||
|
"Get or create a Dijkstra distance map for a root position.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" root: Root position as Vector, Entity, or (x, y) tuple\n"
|
||||||
|
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" DijkstraMap object for querying distances and paths.\n\n"
|
||||||
|
"Grid caches DijkstraMaps by root position. Multiple requests for the\n"
|
||||||
|
"same root return the same cached map. Call clear_dijkstra_maps() after\n"
|
||||||
|
"changing grid walkability to invalidate the cache."},
|
||||||
|
{"clear_dijkstra_maps", (PyCFunction)UIGridPathfinding::Grid_clear_dijkstra_maps, METH_NOARGS,
|
||||||
|
"clear_dijkstra_maps() -> None\n\n"
|
||||||
|
"Clear all cached Dijkstra maps.\n\n"
|
||||||
|
"Call this after modifying grid cell walkability to ensure pathfinding\n"
|
||||||
|
"uses updated walkability data."},
|
||||||
{"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS | METH_KEYWORDS,
|
{"add_layer", (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,6 +8,8 @@
|
||||||
#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"
|
||||||
|
|
@ -25,6 +27,9 @@
|
||||||
#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:
|
||||||
|
|
@ -33,10 +38,13 @@ 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);
|
||||||
|
|
@ -54,15 +62,12 @@ 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 methods
|
// Pathfinding - new API creates AStarPath/DijkstraMap objects
|
||||||
std::vector<std::pair<int, int>> findPath(int x1, int y1, int x2, int y2, float diagonalCost = 1.41f);
|
// See UIGridPathfinding.h for the new pathfinding API
|
||||||
void computeDijkstra(int rootX, int rootY, float diagonalCost = 1.41f);
|
// Grid.find_path() now returns AStarPath objects
|
||||||
float getDijkstraDistance(int x, int y) const;
|
// Grid.get_dijkstra_map() returns DijkstraMap objects (cached by root position)
|
||||||
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;
|
||||||
|
|
@ -167,11 +172,10 @@ 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);
|
||||||
static PyObject* py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
// Pathfinding methods moved to UIGridPathfinding.cpp
|
||||||
static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
// py_find_path -> UIGridPathfinding::Grid_find_path (returns AStarPath)
|
||||||
static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
// py_get_dijkstra_map -> UIGridPathfinding::Grid_get_dijkstra_map (returns DijkstraMap)
|
||||||
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
// py_clear_dijkstra_maps -> UIGridPathfinding::Grid_clear_dijkstra_maps
|
||||||
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
|
||||||
|
|
||||||
|
|
|
||||||
688
src/UIGridPathfinding.cpp
Normal file
688
src/UIGridPathfinding.cpp
Normal file
|
|
@ -0,0 +1,688 @@
|
||||||
|
#include "UIGridPathfinding.h"
|
||||||
|
#include "UIGrid.h"
|
||||||
|
#include "UIEntity.h"
|
||||||
|
#include "PyVector.h"
|
||||||
|
#include "McRFPy_API.h"
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// DijkstraMap Implementation
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
DijkstraMap::DijkstraMap(TCODMap* map, int root_x, int root_y, float diag_cost)
|
||||||
|
: tcod_map(map)
|
||||||
|
, root(root_x, root_y)
|
||||||
|
, diagonal_cost(diag_cost)
|
||||||
|
{
|
||||||
|
tcod_dijkstra = new TCODDijkstra(tcod_map, diagonal_cost);
|
||||||
|
tcod_dijkstra->compute(root_x, root_y); // Compute immediately at creation
|
||||||
|
}
|
||||||
|
|
||||||
|
DijkstraMap::~DijkstraMap() {
|
||||||
|
if (tcod_dijkstra) {
|
||||||
|
delete tcod_dijkstra;
|
||||||
|
tcod_dijkstra = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float DijkstraMap::getDistance(int x, int y) const {
|
||||||
|
if (!tcod_dijkstra) return -1.0f;
|
||||||
|
return tcod_dijkstra->getDistance(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<sf::Vector2i> DijkstraMap::getPathFrom(int x, int y) const {
|
||||||
|
std::vector<sf::Vector2i> path;
|
||||||
|
if (!tcod_dijkstra) return path;
|
||||||
|
|
||||||
|
if (tcod_dijkstra->setPath(x, y)) {
|
||||||
|
int px, py;
|
||||||
|
while (tcod_dijkstra->walk(&px, &py)) {
|
||||||
|
path.push_back(sf::Vector2i(px, py));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::Vector2i DijkstraMap::stepFrom(int x, int y, bool* valid) const {
|
||||||
|
if (!tcod_dijkstra) {
|
||||||
|
if (valid) *valid = false;
|
||||||
|
return sf::Vector2i(-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tcod_dijkstra->setPath(x, y)) {
|
||||||
|
if (valid) *valid = false;
|
||||||
|
return sf::Vector2i(-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int px, py;
|
||||||
|
if (tcod_dijkstra->walk(&px, &py)) {
|
||||||
|
if (valid) *valid = true;
|
||||||
|
return sf::Vector2i(px, py);
|
||||||
|
}
|
||||||
|
|
||||||
|
// At root or no path
|
||||||
|
if (valid) *valid = false;
|
||||||
|
return sf::Vector2i(-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Helper Functions
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
bool UIGridPathfinding::ExtractPosition(PyObject* obj, int* x, int* y,
|
||||||
|
UIGrid* expected_grid,
|
||||||
|
const char* arg_name) {
|
||||||
|
// Get types from module to avoid static type instance issues
|
||||||
|
PyObject* entity_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
||||||
|
PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||||
|
|
||||||
|
// Check if it's an Entity
|
||||||
|
if (entity_type && PyObject_IsInstance(obj, entity_type)) {
|
||||||
|
Py_DECREF(entity_type);
|
||||||
|
Py_XDECREF(vector_type);
|
||||||
|
auto* entity = (PyUIEntityObject*)obj;
|
||||||
|
if (!entity->data) {
|
||||||
|
PyErr_Format(PyExc_RuntimeError,
|
||||||
|
"%s: Entity has no data", arg_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!entity->data->grid) {
|
||||||
|
PyErr_Format(PyExc_RuntimeError,
|
||||||
|
"%s: Entity is not attached to any grid", arg_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (expected_grid && entity->data->grid.get() != expected_grid) {
|
||||||
|
PyErr_Format(PyExc_RuntimeError,
|
||||||
|
"%s: Entity belongs to a different grid", arg_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*x = static_cast<int>(entity->data->position.x);
|
||||||
|
*y = static_cast<int>(entity->data->position.y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Py_XDECREF(entity_type);
|
||||||
|
|
||||||
|
// Check if it's a Vector
|
||||||
|
if (vector_type && PyObject_IsInstance(obj, vector_type)) {
|
||||||
|
Py_DECREF(vector_type);
|
||||||
|
auto* vec = (PyVectorObject*)obj;
|
||||||
|
*x = static_cast<int>(vec->data.x);
|
||||||
|
*y = static_cast<int>(vec->data.y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Py_XDECREF(vector_type);
|
||||||
|
|
||||||
|
// Try tuple/list
|
||||||
|
if (PySequence_Check(obj) && PySequence_Size(obj) == 2) {
|
||||||
|
PyObject* x_obj = PySequence_GetItem(obj, 0);
|
||||||
|
PyObject* y_obj = PySequence_GetItem(obj, 1);
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
if (x_obj && y_obj && PyNumber_Check(x_obj) && PyNumber_Check(y_obj)) {
|
||||||
|
PyObject* x_long = PyNumber_Long(x_obj);
|
||||||
|
PyObject* y_long = PyNumber_Long(y_obj);
|
||||||
|
if (x_long && y_long) {
|
||||||
|
*x = PyLong_AsLong(x_long);
|
||||||
|
*y = PyLong_AsLong(y_long);
|
||||||
|
ok = !PyErr_Occurred();
|
||||||
|
Py_DECREF(x_long);
|
||||||
|
Py_DECREF(y_long);
|
||||||
|
}
|
||||||
|
Py_XDECREF(x_long);
|
||||||
|
Py_XDECREF(y_long);
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_XDECREF(x_obj);
|
||||||
|
Py_XDECREF(y_obj);
|
||||||
|
|
||||||
|
if (ok) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyErr_Format(PyExc_TypeError,
|
||||||
|
"%s: expected Vector, Entity, or (x, y) tuple", arg_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// AStarPath Python Methods
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::AStarPath_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
|
||||||
|
PyAStarPathObject* self = (PyAStarPathObject*)type->tp_alloc(type, 0);
|
||||||
|
if (self) {
|
||||||
|
new (&self->path) std::vector<sf::Vector2i>(); // Placement new
|
||||||
|
self->current_index = 0;
|
||||||
|
self->origin = sf::Vector2i(0, 0);
|
||||||
|
self->destination = sf::Vector2i(0, 0);
|
||||||
|
}
|
||||||
|
return (PyObject*)self;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGridPathfinding::AStarPath_init(PyAStarPathObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
// AStarPath should not be created directly from Python
|
||||||
|
PyErr_SetString(PyExc_TypeError,
|
||||||
|
"AStarPath cannot be instantiated directly. Use Grid.find_path() instead.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIGridPathfinding::AStarPath_dealloc(PyAStarPathObject* self) {
|
||||||
|
self->path.~vector(); // Explicitly destroy
|
||||||
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::AStarPath_repr(PyAStarPathObject* self) {
|
||||||
|
size_t remaining = self->path.size() - self->current_index;
|
||||||
|
return PyUnicode_FromFormat("<AStarPath from (%d,%d) to (%d,%d), %zu steps remaining>",
|
||||||
|
self->origin.x, self->origin.y,
|
||||||
|
self->destination.x, self->destination.y,
|
||||||
|
remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::AStarPath_walk(PyAStarPathObject* self, PyObject* args) {
|
||||||
|
if (self->current_index >= self->path.size()) {
|
||||||
|
PyErr_SetString(PyExc_IndexError, "Path exhausted - no more steps");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::Vector2i pos = self->path[self->current_index++];
|
||||||
|
return PyVector(sf::Vector2f(static_cast<float>(pos.x), static_cast<float>(pos.y))).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::AStarPath_peek(PyAStarPathObject* self, PyObject* args) {
|
||||||
|
if (self->current_index >= self->path.size()) {
|
||||||
|
PyErr_SetString(PyExc_IndexError, "Path exhausted - no more steps");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::Vector2i pos = self->path[self->current_index];
|
||||||
|
return PyVector(sf::Vector2f(static_cast<float>(pos.x), static_cast<float>(pos.y))).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::AStarPath_get_origin(PyAStarPathObject* self, void* closure) {
|
||||||
|
return PyVector(sf::Vector2f(static_cast<float>(self->origin.x),
|
||||||
|
static_cast<float>(self->origin.y))).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::AStarPath_get_destination(PyAStarPathObject* self, void* closure) {
|
||||||
|
return PyVector(sf::Vector2f(static_cast<float>(self->destination.x),
|
||||||
|
static_cast<float>(self->destination.y))).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::AStarPath_get_remaining(PyAStarPathObject* self, void* closure) {
|
||||||
|
size_t remaining = self->path.size() - self->current_index;
|
||||||
|
return PyLong_FromSize_t(remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_ssize_t UIGridPathfinding::AStarPath_len(PyAStarPathObject* self) {
|
||||||
|
return static_cast<Py_ssize_t>(self->path.size() - self->current_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGridPathfinding::AStarPath_bool(PyObject* obj) {
|
||||||
|
PyAStarPathObject* self = (PyAStarPathObject*)obj;
|
||||||
|
return self->current_index < self->path.size() ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::AStarPath_iter(PyAStarPathObject* self) {
|
||||||
|
// Create iterator object
|
||||||
|
mcrfpydef::PyAStarPathIterObject* iter = PyObject_New(
|
||||||
|
mcrfpydef::PyAStarPathIterObject, &mcrfpydef::PyAStarPathIterType);
|
||||||
|
if (!iter) return NULL;
|
||||||
|
|
||||||
|
Py_INCREF(self);
|
||||||
|
iter->path = self;
|
||||||
|
iter->iter_index = self->current_index;
|
||||||
|
|
||||||
|
return (PyObject*)iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterator implementation
|
||||||
|
static void AStarPathIter_dealloc(mcrfpydef::PyAStarPathIterObject* self) {
|
||||||
|
Py_XDECREF(self->path);
|
||||||
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* AStarPathIter_next(mcrfpydef::PyAStarPathIterObject* self) {
|
||||||
|
if (!self->path || self->iter_index >= self->path->path.size()) {
|
||||||
|
return NULL; // StopIteration
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::Vector2i pos = self->path->path[self->iter_index++];
|
||||||
|
// Note: Iterating is consuming for this iterator
|
||||||
|
self->path->current_index = self->iter_index;
|
||||||
|
|
||||||
|
return PyVector(sf::Vector2f(static_cast<float>(pos.x), static_cast<float>(pos.y))).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* AStarPathIter_iter(mcrfpydef::PyAStarPathIterObject* self) {
|
||||||
|
Py_INCREF(self);
|
||||||
|
return (PyObject*)self;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// DijkstraMap Python Methods
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::DijkstraMap_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {
|
||||||
|
PyDijkstraMapObject* self = (PyDijkstraMapObject*)type->tp_alloc(type, 0);
|
||||||
|
if (self) {
|
||||||
|
new (&self->data) std::shared_ptr<DijkstraMap>();
|
||||||
|
}
|
||||||
|
return (PyObject*)self;
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGridPathfinding::DijkstraMap_init(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
PyErr_SetString(PyExc_TypeError,
|
||||||
|
"DijkstraMap cannot be instantiated directly. Use Grid.get_dijkstra_map() instead.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UIGridPathfinding::DijkstraMap_dealloc(PyDijkstraMapObject* self) {
|
||||||
|
self->data.~shared_ptr();
|
||||||
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::DijkstraMap_repr(PyDijkstraMapObject* self) {
|
||||||
|
if (!self->data) {
|
||||||
|
return PyUnicode_FromString("<DijkstraMap (invalid)>");
|
||||||
|
}
|
||||||
|
sf::Vector2i root = self->data->getRoot();
|
||||||
|
return PyUnicode_FromFormat("<DijkstraMap root=(%d,%d)>", root.x, root.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::DijkstraMap_distance(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"pos", NULL};
|
||||||
|
PyObject* pos_obj = NULL;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(kwlist), &pos_obj)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x, y;
|
||||||
|
if (!ExtractPosition(pos_obj, &x, &y, nullptr, "pos")) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dist = self->data->getDistance(x, y);
|
||||||
|
if (dist < 0) {
|
||||||
|
Py_RETURN_NONE; // Unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
return PyFloat_FromDouble(dist);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::DijkstraMap_path_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"pos", NULL};
|
||||||
|
PyObject* pos_obj = NULL;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(kwlist), &pos_obj)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x, y;
|
||||||
|
if (!ExtractPosition(pos_obj, &x, &y, nullptr, "pos")) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<sf::Vector2i> path = self->data->getPathFrom(x, y);
|
||||||
|
|
||||||
|
// Create an AStarPath object to return
|
||||||
|
PyAStarPathObject* result = (PyAStarPathObject*)mcrfpydef::PyAStarPathType.tp_alloc(
|
||||||
|
&mcrfpydef::PyAStarPathType, 0);
|
||||||
|
if (!result) return NULL;
|
||||||
|
|
||||||
|
new (&result->path) std::vector<sf::Vector2i>(std::move(path));
|
||||||
|
result->current_index = 0;
|
||||||
|
result->origin = sf::Vector2i(x, y);
|
||||||
|
result->destination = self->data->getRoot();
|
||||||
|
|
||||||
|
return (PyObject*)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::DijkstraMap_step_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"pos", NULL};
|
||||||
|
PyObject* pos_obj = NULL;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast<char**>(kwlist), &pos_obj)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x, y;
|
||||||
|
if (!ExtractPosition(pos_obj, &x, &y, nullptr, "pos")) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool valid = false;
|
||||||
|
sf::Vector2i step = self->data->stepFrom(x, y, &valid);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
Py_RETURN_NONE; // At root or unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
return PyVector(sf::Vector2f(static_cast<float>(step.x), static_cast<float>(step.y))).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::DijkstraMap_get_root(PyDijkstraMapObject* self, void* closure) {
|
||||||
|
if (!self->data) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
sf::Vector2i root = self->data->getRoot();
|
||||||
|
return PyVector(sf::Vector2f(static_cast<float>(root.x), static_cast<float>(root.y))).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Grid Factory Methods
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::Grid_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL};
|
||||||
|
PyObject* start_obj = NULL;
|
||||||
|
PyObject* end_obj = NULL;
|
||||||
|
float diagonal_cost = 1.41f;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast<char**>(kwlist),
|
||||||
|
&start_obj, &end_obj, &diagonal_cost)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Grid is invalid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x1, y1, x2, y2;
|
||||||
|
if (!ExtractPosition(start_obj, &x1, &y1, self->data.get(), "start")) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (!ExtractPosition(end_obj, &x2, &y2, self->data.get(), "end")) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds check
|
||||||
|
if (x1 < 0 || x1 >= self->data->grid_w || y1 < 0 || y1 >= self->data->grid_h ||
|
||||||
|
x2 < 0 || x2 >= self->data->grid_w || y2 < 0 || y2 >= self->data->grid_h) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Position out of grid bounds");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute path using temporary TCODPath
|
||||||
|
TCODPath tcod_path(self->data->getTCODMap(), diagonal_cost);
|
||||||
|
if (!tcod_path.compute(x1, y1, x2, y2)) {
|
||||||
|
Py_RETURN_NONE; // No path exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create AStarPath result object
|
||||||
|
PyAStarPathObject* result = (PyAStarPathObject*)mcrfpydef::PyAStarPathType.tp_alloc(
|
||||||
|
&mcrfpydef::PyAStarPathType, 0);
|
||||||
|
if (!result) return NULL;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
new (&result->path) std::vector<sf::Vector2i>();
|
||||||
|
result->current_index = 0;
|
||||||
|
result->origin = sf::Vector2i(x1, y1);
|
||||||
|
result->destination = sf::Vector2i(x2, y2);
|
||||||
|
|
||||||
|
// Copy path data
|
||||||
|
result->path.reserve(tcod_path.size());
|
||||||
|
for (int i = 0; i < tcod_path.size(); i++) {
|
||||||
|
int px, py;
|
||||||
|
tcod_path.get(i, &px, &py);
|
||||||
|
result->path.push_back(sf::Vector2i(px, py));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (PyObject*)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::Grid_get_dijkstra_map(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"root", "diagonal_cost", NULL};
|
||||||
|
PyObject* root_obj = NULL;
|
||||||
|
float diagonal_cost = 1.41f;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(kwlist),
|
||||||
|
&root_obj, &diagonal_cost)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Grid is invalid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int root_x, root_y;
|
||||||
|
if (!ExtractPosition(root_obj, &root_x, &root_y, self->data.get(), "root")) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds check
|
||||||
|
if (root_x < 0 || root_x >= self->data->grid_w || root_y < 0 || root_y >= self->data->grid_h) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Root position out of grid bounds");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto key = std::make_pair(root_x, root_y);
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
auto it = self->data->dijkstra_maps.find(key);
|
||||||
|
if (it != self->data->dijkstra_maps.end()) {
|
||||||
|
// Check diagonal cost matches (or we could ignore this)
|
||||||
|
if (std::abs(it->second->getDiagonalCost() - diagonal_cost) < 0.001f) {
|
||||||
|
// Return existing
|
||||||
|
PyDijkstraMapObject* result = (PyDijkstraMapObject*)mcrfpydef::PyDijkstraMapType.tp_alloc(
|
||||||
|
&mcrfpydef::PyDijkstraMapType, 0);
|
||||||
|
if (!result) return NULL;
|
||||||
|
new (&result->data) std::shared_ptr<DijkstraMap>(it->second);
|
||||||
|
return (PyObject*)result;
|
||||||
|
}
|
||||||
|
// Different diagonal cost - remove old one
|
||||||
|
self->data->dijkstra_maps.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new DijkstraMap
|
||||||
|
auto dijkstra = std::make_shared<DijkstraMap>(
|
||||||
|
self->data->getTCODMap(), root_x, root_y, diagonal_cost);
|
||||||
|
|
||||||
|
// Cache it
|
||||||
|
self->data->dijkstra_maps[key] = dijkstra;
|
||||||
|
|
||||||
|
// Return Python wrapper
|
||||||
|
PyDijkstraMapObject* result = (PyDijkstraMapObject*)mcrfpydef::PyDijkstraMapType.tp_alloc(
|
||||||
|
&mcrfpydef::PyDijkstraMapType, 0);
|
||||||
|
if (!result) return NULL;
|
||||||
|
|
||||||
|
new (&result->data) std::shared_ptr<DijkstraMap>(dijkstra);
|
||||||
|
return (PyObject*)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* UIGridPathfinding::Grid_clear_dijkstra_maps(PyUIGridObject* self, PyObject* args) {
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Grid is invalid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->dijkstra_maps.clear();
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Python Type Definitions
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
namespace mcrfpydef {
|
||||||
|
|
||||||
|
// AStarPath methods
|
||||||
|
PyMethodDef PyAStarPath_methods[] = {
|
||||||
|
{"walk", (PyCFunction)UIGridPathfinding::AStarPath_walk, METH_NOARGS,
|
||||||
|
"walk() -> Vector\n\n"
|
||||||
|
"Get and consume next step in the path.\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" Next position as Vector.\n\n"
|
||||||
|
"Raises:\n"
|
||||||
|
" IndexError: If path is exhausted."},
|
||||||
|
|
||||||
|
{"peek", (PyCFunction)UIGridPathfinding::AStarPath_peek, METH_NOARGS,
|
||||||
|
"peek() -> Vector\n\n"
|
||||||
|
"See next step without consuming it.\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" Next position as Vector.\n\n"
|
||||||
|
"Raises:\n"
|
||||||
|
" IndexError: If path is exhausted."},
|
||||||
|
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
// AStarPath getsetters
|
||||||
|
PyGetSetDef PyAStarPath_getsetters[] = {
|
||||||
|
{"origin", (getter)UIGridPathfinding::AStarPath_get_origin, NULL,
|
||||||
|
"Starting position of the path (Vector, read-only).", NULL},
|
||||||
|
|
||||||
|
{"destination", (getter)UIGridPathfinding::AStarPath_get_destination, NULL,
|
||||||
|
"Ending position of the path (Vector, read-only).", NULL},
|
||||||
|
|
||||||
|
{"remaining", (getter)UIGridPathfinding::AStarPath_get_remaining, NULL,
|
||||||
|
"Number of steps remaining in the path (int, read-only).", NULL},
|
||||||
|
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
// AStarPath number methods (for bool)
|
||||||
|
PyNumberMethods PyAStarPath_as_number = {
|
||||||
|
.nb_bool = UIGridPathfinding::AStarPath_bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
// AStarPath sequence methods (for len)
|
||||||
|
PySequenceMethods PyAStarPath_as_sequence = {
|
||||||
|
.sq_length = (lenfunc)UIGridPathfinding::AStarPath_len,
|
||||||
|
};
|
||||||
|
|
||||||
|
// AStarPath type
|
||||||
|
PyTypeObject PyAStarPathType = {
|
||||||
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
.tp_name = "mcrfpy.AStarPath",
|
||||||
|
.tp_basicsize = sizeof(PyAStarPathObject),
|
||||||
|
.tp_itemsize = 0,
|
||||||
|
.tp_dealloc = (destructor)UIGridPathfinding::AStarPath_dealloc,
|
||||||
|
.tp_repr = (reprfunc)UIGridPathfinding::AStarPath_repr,
|
||||||
|
.tp_as_number = &PyAStarPath_as_number,
|
||||||
|
.tp_as_sequence = &PyAStarPath_as_sequence,
|
||||||
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
|
.tp_doc = PyDoc_STR(
|
||||||
|
"A computed A* path result, consumed step by step.\n\n"
|
||||||
|
"Created by Grid.find_path(). Cannot be instantiated directly.\n\n"
|
||||||
|
"Use walk() to get and consume each step, or iterate directly.\n"
|
||||||
|
"Use peek() to see the next step without consuming it.\n"
|
||||||
|
"Use bool(path) or len(path) to check if steps remain.\n\n"
|
||||||
|
"Properties:\n"
|
||||||
|
" origin (Vector): Starting position (read-only)\n"
|
||||||
|
" destination (Vector): Ending position (read-only)\n"
|
||||||
|
" remaining (int): Steps remaining (read-only)\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
" path = grid.find_path(start, end)\n"
|
||||||
|
" if path:\n"
|
||||||
|
" while path:\n"
|
||||||
|
" next_pos = path.walk()\n"
|
||||||
|
" entity.pos = next_pos"),
|
||||||
|
.tp_iter = (getiterfunc)UIGridPathfinding::AStarPath_iter,
|
||||||
|
.tp_methods = PyAStarPath_methods,
|
||||||
|
.tp_getset = PyAStarPath_getsetters,
|
||||||
|
.tp_init = (initproc)UIGridPathfinding::AStarPath_init,
|
||||||
|
.tp_new = UIGridPathfinding::AStarPath_new,
|
||||||
|
};
|
||||||
|
|
||||||
|
// AStarPath iterator type
|
||||||
|
PyTypeObject PyAStarPathIterType = {
|
||||||
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
.tp_name = "mcrfpy.AStarPathIterator",
|
||||||
|
.tp_basicsize = sizeof(PyAStarPathIterObject),
|
||||||
|
.tp_itemsize = 0,
|
||||||
|
.tp_dealloc = (destructor)AStarPathIter_dealloc,
|
||||||
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
|
.tp_iter = (getiterfunc)AStarPathIter_iter,
|
||||||
|
.tp_iternext = (iternextfunc)AStarPathIter_next,
|
||||||
|
};
|
||||||
|
|
||||||
|
// DijkstraMap methods
|
||||||
|
PyMethodDef PyDijkstraMap_methods[] = {
|
||||||
|
{"distance", (PyCFunction)UIGridPathfinding::DijkstraMap_distance, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"distance(pos) -> float | None\n\n"
|
||||||
|
"Get distance from position to root.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos: Position as Vector, Entity, or (x, y) tuple.\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" Float distance, or None if position is unreachable."},
|
||||||
|
|
||||||
|
{"path_from", (PyCFunction)UIGridPathfinding::DijkstraMap_path_from, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"path_from(pos) -> AStarPath\n\n"
|
||||||
|
"Get full path from position to root.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos: Starting position as Vector, Entity, or (x, y) tuple.\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" AStarPath from pos toward root."},
|
||||||
|
|
||||||
|
{"step_from", (PyCFunction)UIGridPathfinding::DijkstraMap_step_from, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"step_from(pos) -> Vector | None\n\n"
|
||||||
|
"Get single step from position toward root.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos: Current position as Vector, Entity, or (x, y) tuple.\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" Next position as Vector, or None if at root or unreachable."},
|
||||||
|
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DijkstraMap getsetters
|
||||||
|
PyGetSetDef PyDijkstraMap_getsetters[] = {
|
||||||
|
{"root", (getter)UIGridPathfinding::DijkstraMap_get_root, NULL,
|
||||||
|
"Root position that distances are measured from (Vector, read-only).", NULL},
|
||||||
|
|
||||||
|
{NULL}
|
||||||
|
};
|
||||||
|
|
||||||
|
// DijkstraMap type
|
||||||
|
PyTypeObject PyDijkstraMapType = {
|
||||||
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
.tp_name = "mcrfpy.DijkstraMap",
|
||||||
|
.tp_basicsize = sizeof(PyDijkstraMapObject),
|
||||||
|
.tp_itemsize = 0,
|
||||||
|
.tp_dealloc = (destructor)UIGridPathfinding::DijkstraMap_dealloc,
|
||||||
|
.tp_repr = (reprfunc)UIGridPathfinding::DijkstraMap_repr,
|
||||||
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
|
.tp_doc = PyDoc_STR(
|
||||||
|
"A Dijkstra distance map from a fixed root position.\n\n"
|
||||||
|
"Created by Grid.get_dijkstra_map(). Cannot be instantiated directly.\n\n"
|
||||||
|
"Grid caches these maps - multiple requests for the same root return\n"
|
||||||
|
"the same map. Call Grid.clear_dijkstra_maps() after changing grid\n"
|
||||||
|
"walkability to invalidate the cache.\n\n"
|
||||||
|
"Properties:\n"
|
||||||
|
" root (Vector): Root position (read-only)\n\n"
|
||||||
|
"Methods:\n"
|
||||||
|
" distance(pos) -> float | None: Get distance to root\n"
|
||||||
|
" path_from(pos) -> AStarPath: Get full path to root\n"
|
||||||
|
" step_from(pos) -> Vector | None: Get single step toward root\n\n"
|
||||||
|
"Example:\n"
|
||||||
|
" dijkstra = grid.get_dijkstra_map(player.pos)\n"
|
||||||
|
" for enemy in enemies:\n"
|
||||||
|
" dist = dijkstra.distance(enemy.pos)\n"
|
||||||
|
" if dist and dist < 10:\n"
|
||||||
|
" step = dijkstra.step_from(enemy.pos)\n"
|
||||||
|
" if step:\n"
|
||||||
|
" enemy.pos = step"),
|
||||||
|
.tp_methods = PyDijkstraMap_methods,
|
||||||
|
.tp_getset = PyDijkstraMap_getsetters,
|
||||||
|
.tp_init = (initproc)UIGridPathfinding::DijkstraMap_init,
|
||||||
|
.tp_new = UIGridPathfinding::DijkstraMap_new,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mcrfpydef
|
||||||
152
src/UIGridPathfinding.h
Normal file
152
src/UIGridPathfinding.h
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Common.h"
|
||||||
|
#include "Python.h"
|
||||||
|
#include "UIBase.h" // For PyUIGridObject typedef
|
||||||
|
#include <libtcod.h>
|
||||||
|
#include <SFML/System/Vector2.hpp>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
class UIGrid;
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// AStarPath - A computed A* path result, consumed like an iterator
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
struct PyAStarPathObject {
|
||||||
|
PyObject_HEAD
|
||||||
|
std::vector<sf::Vector2i> path; // Pre-computed path positions
|
||||||
|
size_t current_index; // Next step to return
|
||||||
|
sf::Vector2i origin; // Fixed at creation
|
||||||
|
sf::Vector2i destination; // Fixed at creation
|
||||||
|
};
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// DijkstraMap - A Dijkstra distance field from a fixed root
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
class DijkstraMap {
|
||||||
|
public:
|
||||||
|
DijkstraMap(TCODMap* map, int root_x, int root_y, float diagonal_cost);
|
||||||
|
~DijkstraMap();
|
||||||
|
|
||||||
|
// Non-copyable (owns TCODDijkstra)
|
||||||
|
DijkstraMap(const DijkstraMap&) = delete;
|
||||||
|
DijkstraMap& operator=(const DijkstraMap&) = delete;
|
||||||
|
|
||||||
|
// Queries
|
||||||
|
float getDistance(int x, int y) const;
|
||||||
|
std::vector<sf::Vector2i> getPathFrom(int x, int y) const;
|
||||||
|
sf::Vector2i stepFrom(int x, int y, bool* valid = nullptr) const;
|
||||||
|
|
||||||
|
// Accessors
|
||||||
|
sf::Vector2i getRoot() const { return root; }
|
||||||
|
float getDiagonalCost() const { return diagonal_cost; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
TCODDijkstra* tcod_dijkstra; // Owned by this object
|
||||||
|
TCODMap* tcod_map; // Borrowed from Grid
|
||||||
|
sf::Vector2i root;
|
||||||
|
float diagonal_cost;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PyDijkstraMapObject {
|
||||||
|
PyObject_HEAD
|
||||||
|
std::shared_ptr<DijkstraMap> data; // Shared with Grid's collection
|
||||||
|
};
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Helper Functions
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
namespace UIGridPathfinding {
|
||||||
|
// Extract grid position from Vector, Entity, or tuple
|
||||||
|
// Sets Python error and returns false on failure
|
||||||
|
// If expected_grid is provided and obj is Entity, validates grid membership
|
||||||
|
bool ExtractPosition(PyObject* obj, int* x, int* y,
|
||||||
|
UIGrid* expected_grid = nullptr,
|
||||||
|
const char* arg_name = "position");
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// AStarPath Python Type Methods
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
PyObject* AStarPath_new(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||||
|
int AStarPath_init(PyAStarPathObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
void AStarPath_dealloc(PyAStarPathObject* self);
|
||||||
|
PyObject* AStarPath_repr(PyAStarPathObject* self);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
PyObject* AStarPath_walk(PyAStarPathObject* self, PyObject* args);
|
||||||
|
PyObject* AStarPath_peek(PyAStarPathObject* self, PyObject* args);
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
PyObject* AStarPath_get_origin(PyAStarPathObject* self, void* closure);
|
||||||
|
PyObject* AStarPath_get_destination(PyAStarPathObject* self, void* closure);
|
||||||
|
PyObject* AStarPath_get_remaining(PyAStarPathObject* self, void* closure);
|
||||||
|
|
||||||
|
// Sequence protocol
|
||||||
|
Py_ssize_t AStarPath_len(PyAStarPathObject* self);
|
||||||
|
int AStarPath_bool(PyObject* self);
|
||||||
|
PyObject* AStarPath_iter(PyAStarPathObject* self);
|
||||||
|
PyObject* AStarPath_iternext(PyAStarPathObject* self);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// DijkstraMap Python Type Methods
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
PyObject* DijkstraMap_new(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||||
|
int DijkstraMap_init(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
void DijkstraMap_dealloc(PyDijkstraMapObject* self);
|
||||||
|
PyObject* DijkstraMap_repr(PyDijkstraMapObject* self);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
PyObject* DijkstraMap_distance(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
PyObject* DijkstraMap_path_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
PyObject* DijkstraMap_step_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
PyObject* DijkstraMap_get_root(PyDijkstraMapObject* self, void* closure);
|
||||||
|
|
||||||
|
//=========================================================================
|
||||||
|
// Grid Factory Methods (called from UIGrid Python bindings)
|
||||||
|
//=========================================================================
|
||||||
|
|
||||||
|
// Grid.find_path() -> AStarPath | None
|
||||||
|
PyObject* Grid_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
|
// Grid.get_dijkstra_map() -> DijkstraMap
|
||||||
|
PyObject* Grid_get_dijkstra_map(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
|
// Grid.clear_dijkstra_maps() -> None
|
||||||
|
PyObject* Grid_clear_dijkstra_maps(PyUIGridObject* self, PyObject* args);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Python Type Definitions
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
namespace mcrfpydef {
|
||||||
|
|
||||||
|
// AStarPath iterator type
|
||||||
|
struct PyAStarPathIterObject {
|
||||||
|
PyObject_HEAD
|
||||||
|
PyAStarPathObject* path; // Reference to path being iterated
|
||||||
|
size_t iter_index; // Current iteration position
|
||||||
|
};
|
||||||
|
|
||||||
|
extern PyNumberMethods PyAStarPath_as_number;
|
||||||
|
extern PySequenceMethods PyAStarPath_as_sequence;
|
||||||
|
extern PyMethodDef PyAStarPath_methods[];
|
||||||
|
extern PyGetSetDef PyAStarPath_getsetters[];
|
||||||
|
|
||||||
|
extern PyTypeObject PyAStarPathType;
|
||||||
|
extern PyTypeObject PyAStarPathIterType;
|
||||||
|
|
||||||
|
extern PyMethodDef PyDijkstraMap_methods[];
|
||||||
|
extern PyGetSetDef PyDijkstraMap_getsetters[];
|
||||||
|
|
||||||
|
extern PyTypeObject PyDijkstraMapType;
|
||||||
|
}
|
||||||
268
tests/unit/test_heightmap_basic.py
Normal file
268
tests/unit/test_heightmap_basic.py
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
#!/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