From b22dfe95242c66355ea7faa25ac2898e9737bdd8 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Tue, 13 Jan 2026 20:41:23 -0500 Subject: [PATCH] Djikstra to Heightmap: convert pathfinding data into a heightmap for use in procedural generation processes --- src/UIGridPathfinding.cpp | 102 +++++++++++++++++- src/UIGridPathfinding.h | 5 + tests/unit/dijkstra_to_heightmap_test.py | 131 +++++++++++++++++++++++ 3 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 tests/unit/dijkstra_to_heightmap_test.py diff --git a/src/UIGridPathfinding.cpp b/src/UIGridPathfinding.cpp index d232262..2644b7f 100644 --- a/src/UIGridPathfinding.cpp +++ b/src/UIGridPathfinding.cpp @@ -3,6 +3,8 @@ #include "UIEntity.h" #include "PyVector.h" #include "McRFPy_API.h" +#include "PyHeightMap.h" +#include "PyPositionHelper.h" //============================================================================= // DijkstraMap Implementation @@ -12,6 +14,8 @@ 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) + , map_width(map ? map->getWidth() : 0) + , map_height(map ? map->getHeight() : 0) { tcod_dijkstra = new TCODDijkstra(tcod_map, diagonal_cost); tcod_dijkstra->compute(root_x, root_y); // Compute immediately at creation @@ -29,6 +33,14 @@ float DijkstraMap::getDistance(int x, int y) const { return tcod_dijkstra->getDistance(x, y); } +int DijkstraMap::getWidth() const { + return map_width; +} + +int DijkstraMap::getHeight() const { + return map_height; +} + std::vector DijkstraMap::getPathFrom(int x, int y) const { std::vector path; if (!tcod_dijkstra) return path; @@ -124,8 +136,6 @@ bool UIGridPathfinding::ExtractPosition(PyObject* obj, int* x, int* y, *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); @@ -383,6 +393,83 @@ PyObject* UIGridPathfinding::DijkstraMap_get_root(PyDijkstraMapObject* self, voi return PyVector(sf::Vector2f(static_cast(root.x), static_cast(root.y))).pyObject(); } +PyObject* UIGridPathfinding::DijkstraMap_to_heightmap(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"size", "unreachable", nullptr}; + PyObject* size_obj = nullptr; + float unreachable = -1.0f; // Value for cells that can't reach root (distinct from 0 = root) + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Of", const_cast(kwlist), + &size_obj, &unreachable)) { + return nullptr; + } + + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "DijkstraMap is invalid"); + return nullptr; + } + + // Determine output size (default to dijkstra dimensions) + int width = self->data->getWidth(); + int height = self->data->getHeight(); + + if (width <= 0 || height <= 0) { + PyErr_SetString(PyExc_RuntimeError, "DijkstraMap has invalid dimensions"); + return nullptr; + } + + if (size_obj && size_obj != Py_None) { + if (!PyPosition_FromObjectInt(size_obj, &width, &height)) { + PyErr_SetString(PyExc_TypeError, "size must be (width, height) tuple, list, or Vector"); + return nullptr; + } + if (width <= 0 || height <= 0) { + PyErr_SetString(PyExc_ValueError, "size values must be positive"); + return nullptr; + } + } + + // Create HeightMap via Python API (same pattern as BSP.to_heightmap) + PyObject* hmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap"); + if (!hmap_type) { + PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found"); + return nullptr; + } + + PyObject* size_tuple = Py_BuildValue("(ii)", width, height); + PyObject* hmap_args = PyTuple_Pack(1, size_tuple); + Py_DECREF(size_tuple); + + PyHeightMapObject* hmap = (PyHeightMapObject*)PyObject_Call(hmap_type, hmap_args, nullptr); + Py_DECREF(hmap_args); + Py_DECREF(hmap_type); + + if (!hmap) { + return nullptr; + } + + // Get the dijkstra dimensions for bounds checking + int dijkstra_w = self->data->getWidth(); + int dijkstra_h = self->data->getHeight(); + + // Fill heightmap with distance values + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + float dist; + if (x < dijkstra_w && y < dijkstra_h) { + dist = self->data->getDistance(x, y); + if (dist < 0) { + dist = unreachable; // Unreachable cell + } + } else { + dist = unreachable; // Outside dijkstra bounds + } + TCOD_heightmap_set_value(hmap->heightmap, x, y, dist); + } + } + + return (PyObject*)hmap; +} + //============================================================================= // Grid Factory Methods //============================================================================= @@ -639,6 +726,17 @@ PyMethodDef PyDijkstraMap_methods[] = { "Returns:\n" " Next position as Vector, or None if at root or unreachable."}, + {"to_heightmap", (PyCFunction)UIGridPathfinding::DijkstraMap_to_heightmap, METH_VARARGS | METH_KEYWORDS, + "to_heightmap(size=None, unreachable=-1.0) -> HeightMap\n\n" + "Convert distance field to a HeightMap.\n\n" + "Each cell's height equals its pathfinding distance from the root.\n" + "Useful for visualization, procedural terrain, or influence mapping.\n\n" + "Args:\n" + " size: Optional (width, height) tuple. Defaults to dijkstra dimensions.\n" + " unreachable: Value for cells that cannot reach root (default -1.0).\n\n" + "Returns:\n" + " HeightMap with distance values as heights."}, + {NULL} }; diff --git a/src/UIGridPathfinding.h b/src/UIGridPathfinding.h index 07294d0..2ca88b4 100644 --- a/src/UIGridPathfinding.h +++ b/src/UIGridPathfinding.h @@ -44,12 +44,16 @@ public: // Accessors sf::Vector2i getRoot() const { return root; } float getDiagonalCost() const { return diagonal_cost; } + int getWidth() const; + int getHeight() const; private: TCODDijkstra* tcod_dijkstra; // Owned by this object TCODMap* tcod_map; // Borrowed from Grid sf::Vector2i root; float diagonal_cost; + int map_width; // Cached from TCODMap at construction + int map_height; }; struct PyDijkstraMapObject { @@ -106,6 +110,7 @@ namespace UIGridPathfinding { 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); + PyObject* DijkstraMap_to_heightmap(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds); // Properties PyObject* DijkstraMap_get_root(PyDijkstraMapObject* self, void* closure); diff --git a/tests/unit/dijkstra_to_heightmap_test.py b/tests/unit/dijkstra_to_heightmap_test.py new file mode 100644 index 0000000..8dc651a --- /dev/null +++ b/tests/unit/dijkstra_to_heightmap_test.py @@ -0,0 +1,131 @@ +"""Test DijkstraMap.to_heightmap() method.""" +import mcrfpy +import sys + +def test_basic_conversion(): + """Test basic conversion of DijkstraMap to HeightMap.""" + grid = mcrfpy.Grid(grid_size=(10, 10)) + + # Initialize all cells as walkable + for y in range(10): + for x in range(10): + grid.at((x, y)).walkable = True + + # Get a dijkstra map from center + dijkstra = grid.get_dijkstra_map((5, 5)) + + # Convert to heightmap + hmap = dijkstra.to_heightmap() + + # Verify type + assert type(hmap).__name__ == "HeightMap", f"Expected HeightMap, got {type(hmap).__name__}" + + # Verify root cell has distance 0 + assert hmap[(5, 5)] == 0.0, f"Root cell should have height 0, got {hmap[(5, 5)]}" + + # Verify corner has non-zero distance + corner_dist = dijkstra.distance((0, 0)) + corner_height = hmap[(0, 0)] + assert abs(corner_dist - corner_height) < 0.001, f"Height {corner_height} should match distance {corner_dist}" + + print("test_basic_conversion PASSED") + +def test_unreachable_cells(): + """Test that unreachable cells use the unreachable parameter.""" + grid = mcrfpy.Grid(grid_size=(10, 10)) + + # Initialize all cells as walkable + for y in range(10): + for x in range(10): + grid.at((x, y)).walkable = True + + # Add a wall + grid.at((3, 3)).walkable = False + + dijkstra = grid.get_dijkstra_map((5, 5)) + + # Default unreachable value is -1.0 (distinct from root which has distance 0) + hmap1 = dijkstra.to_heightmap() + assert hmap1[(3, 3)] == -1.0, f"Default unreachable should be -1.0, got {hmap1[(3, 3)]}" + + # Custom unreachable value + hmap2 = dijkstra.to_heightmap(unreachable=0.0) + assert hmap2[(3, 3)] == 0.0, f"Custom unreachable should be 0.0, got {hmap2[(3, 3)]}" + + # Large unreachable value + hmap3 = dijkstra.to_heightmap(unreachable=999.0) + assert hmap3[(3, 3)] == 999.0, f"Large unreachable should be 999.0, got {hmap3[(3, 3)]}" + + print("test_unreachable_cells PASSED") + +def test_custom_size(): + """Test custom size parameter.""" + grid = mcrfpy.Grid(grid_size=(10, 10)) + + for y in range(10): + for x in range(10): + grid.at((x, y)).walkable = True + + dijkstra = grid.get_dijkstra_map((5, 5)) + + # Custom smaller size + hmap = dijkstra.to_heightmap(size=(5, 5)) + + # Verify dimensions via repr + repr_str = repr(hmap) + assert "5 x 5" in repr_str, f"Expected 5x5 heightmap, got {repr_str}" + + # Values within dijkstra bounds should work + assert hmap[(0, 0)] == dijkstra.distance((0, 0)), "Heights should match distances within bounds" + + print("test_custom_size PASSED") + +def test_larger_custom_size(): + """Test custom size larger than dijkstra bounds.""" + grid = mcrfpy.Grid(grid_size=(5, 5)) + + for y in range(5): + for x in range(5): + grid.at((x, y)).walkable = True + + dijkstra = grid.get_dijkstra_map((2, 2)) + + # Custom larger size - cells outside dijkstra bounds get unreachable value + hmap = dijkstra.to_heightmap(size=(10, 10), unreachable=-99.0) + + # Values within dijkstra bounds should work + assert hmap[(2, 2)] == 0.0, "Root should have height 0" + + # Values outside bounds should have unreachable value + assert hmap[(8, 8)] == -99.0, f"Outside bounds should be -99.0, got {hmap[(8, 8)]}" + + print("test_larger_custom_size PASSED") + +def test_distance_values(): + """Test that heightmap values match dijkstra distances.""" + grid = mcrfpy.Grid(grid_size=(10, 10)) + + for y in range(10): + for x in range(10): + grid.at((x, y)).walkable = True + + dijkstra = grid.get_dijkstra_map((0, 0)) + hmap = dijkstra.to_heightmap() + + # Check various positions + for pos in [(0, 0), (1, 0), (0, 1), (5, 5), (9, 9)]: + dist = dijkstra.distance(pos) + height = hmap[pos] + assert abs(dist - height) < 0.001, f"At {pos}: height {height} != distance {dist}" + + print("test_distance_values PASSED") + +# Run all tests +test_basic_conversion() +test_unreachable_cells() +test_custom_size() +test_larger_custom_size() +test_distance_values() + +print("\nAll DijkstraMap.to_heightmap tests PASSED") +sys.exit(0)