Djikstra to Heightmap: convert pathfinding data into a heightmap for use in procedural generation processes
This commit is contained in:
parent
4bf590749c
commit
b22dfe9524
3 changed files with 236 additions and 2 deletions
|
|
@ -3,6 +3,8 @@
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
|
#include "PyHeightMap.h"
|
||||||
|
#include "PyPositionHelper.h"
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// DijkstraMap Implementation
|
// DijkstraMap Implementation
|
||||||
|
|
@ -12,6 +14,8 @@ DijkstraMap::DijkstraMap(TCODMap* map, int root_x, int root_y, float diag_cost)
|
||||||
: tcod_map(map)
|
: tcod_map(map)
|
||||||
, root(root_x, root_y)
|
, root(root_x, root_y)
|
||||||
, diagonal_cost(diag_cost)
|
, 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 = new TCODDijkstra(tcod_map, diagonal_cost);
|
||||||
tcod_dijkstra->compute(root_x, root_y); // Compute immediately at creation
|
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);
|
return tcod_dijkstra->getDistance(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int DijkstraMap::getWidth() const {
|
||||||
|
return map_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
int DijkstraMap::getHeight() const {
|
||||||
|
return map_height;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<sf::Vector2i> DijkstraMap::getPathFrom(int x, int y) const {
|
std::vector<sf::Vector2i> DijkstraMap::getPathFrom(int x, int y) const {
|
||||||
std::vector<sf::Vector2i> path;
|
std::vector<sf::Vector2i> path;
|
||||||
if (!tcod_dijkstra) return path;
|
if (!tcod_dijkstra) return path;
|
||||||
|
|
@ -124,8 +136,6 @@ bool UIGridPathfinding::ExtractPosition(PyObject* obj, int* x, int* y,
|
||||||
*x = PyLong_AsLong(x_long);
|
*x = PyLong_AsLong(x_long);
|
||||||
*y = PyLong_AsLong(y_long);
|
*y = PyLong_AsLong(y_long);
|
||||||
ok = !PyErr_Occurred();
|
ok = !PyErr_Occurred();
|
||||||
Py_DECREF(x_long);
|
|
||||||
Py_DECREF(y_long);
|
|
||||||
}
|
}
|
||||||
Py_XDECREF(x_long);
|
Py_XDECREF(x_long);
|
||||||
Py_XDECREF(y_long);
|
Py_XDECREF(y_long);
|
||||||
|
|
@ -383,6 +393,83 @@ PyObject* UIGridPathfinding::DijkstraMap_get_root(PyDijkstraMapObject* self, voi
|
||||||
return PyVector(sf::Vector2f(static_cast<float>(root.x), static_cast<float>(root.y))).pyObject();
|
return PyVector(sf::Vector2f(static_cast<float>(root.x), static_cast<float>(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<char**>(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
|
// Grid Factory Methods
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
|
|
@ -639,6 +726,17 @@ PyMethodDef PyDijkstraMap_methods[] = {
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" Next position as Vector, or None if at root or unreachable."},
|
" 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}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,12 +44,16 @@ public:
|
||||||
// Accessors
|
// Accessors
|
||||||
sf::Vector2i getRoot() const { return root; }
|
sf::Vector2i getRoot() const { return root; }
|
||||||
float getDiagonalCost() const { return diagonal_cost; }
|
float getDiagonalCost() const { return diagonal_cost; }
|
||||||
|
int getWidth() const;
|
||||||
|
int getHeight() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TCODDijkstra* tcod_dijkstra; // Owned by this object
|
TCODDijkstra* tcod_dijkstra; // Owned by this object
|
||||||
TCODMap* tcod_map; // Borrowed from Grid
|
TCODMap* tcod_map; // Borrowed from Grid
|
||||||
sf::Vector2i root;
|
sf::Vector2i root;
|
||||||
float diagonal_cost;
|
float diagonal_cost;
|
||||||
|
int map_width; // Cached from TCODMap at construction
|
||||||
|
int map_height;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PyDijkstraMapObject {
|
struct PyDijkstraMapObject {
|
||||||
|
|
@ -106,6 +110,7 @@ namespace UIGridPathfinding {
|
||||||
PyObject* DijkstraMap_distance(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
PyObject* DijkstraMap_distance(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
PyObject* DijkstraMap_path_from(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_step_from(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
PyObject* DijkstraMap_to_heightmap(PyDijkstraMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
PyObject* DijkstraMap_get_root(PyDijkstraMapObject* self, void* closure);
|
PyObject* DijkstraMap_get_root(PyDijkstraMapObject* self, void* closure);
|
||||||
|
|
|
||||||
131
tests/unit/dijkstra_to_heightmap_test.py
Normal file
131
tests/unit/dijkstra_to_heightmap_test.py
Normal file
|
|
@ -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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue