diff --git a/src/PyHeightMap.cpp b/src/PyHeightMap.cpp index ba2c1cf..e263d14 100644 --- a/src/PyHeightMap.cpp +++ b/src/PyHeightMap.cpp @@ -60,56 +60,6 @@ PyMethodDef PyHeightMap::methods[] = { MCRF_ARG("max", "Target maximum value (default 1.0)") MCRF_RETURNS("HeightMap: self, for method chaining") )}, - // Query methods (#196) - {"get", (PyCFunction)PyHeightMap::get, METH_VARARGS, - MCRF_METHOD(HeightMap, get, - MCRF_SIG("(pos: tuple[int, int])", "float"), - MCRF_DESC("Get the height value at integer coordinates."), - MCRF_ARGS_START - MCRF_ARG("pos", "Position as (x, y) tuple") - MCRF_RETURNS("float: Height value at that position") - MCRF_RAISES("IndexError", "Position is out of bounds") - )}, - {"get_interpolated", (PyCFunction)PyHeightMap::get_interpolated, METH_VARARGS, - MCRF_METHOD(HeightMap, get_interpolated, - MCRF_SIG("(pos: tuple[float, float])", "float"), - MCRF_DESC("Get interpolated height value at non-integer coordinates."), - MCRF_ARGS_START - MCRF_ARG("pos", "Position as (x, y) tuple with float coordinates") - MCRF_RETURNS("float: Bilinearly interpolated height value") - )}, - {"get_slope", (PyCFunction)PyHeightMap::get_slope, METH_VARARGS, - MCRF_METHOD(HeightMap, get_slope, - MCRF_SIG("(pos: tuple[int, int])", "float"), - MCRF_DESC("Get the slope at integer coordinates, from 0 (flat) to pi/2 (vertical)."), - MCRF_ARGS_START - MCRF_ARG("pos", "Position as (x, y) tuple") - MCRF_RETURNS("float: Slope angle in radians (0 to pi/2)") - MCRF_RAISES("IndexError", "Position is out of bounds") - )}, - {"get_normal", (PyCFunction)PyHeightMap::get_normal, METH_VARARGS | METH_KEYWORDS, - MCRF_METHOD(HeightMap, get_normal, - MCRF_SIG("(pos: tuple[float, float], water_level: float = 0.0)", "tuple[float, float, float]"), - MCRF_DESC("Get the normal vector at given coordinates for lighting calculations."), - MCRF_ARGS_START - MCRF_ARG("pos", "Position as (x, y) tuple with float coordinates") - MCRF_ARG("water_level", "Water level below which terrain is considered flat (default 0.0)") - MCRF_RETURNS("tuple[float, float, float]: Normal vector (nx, ny, nz)") - )}, - {"min_max", (PyCFunction)PyHeightMap::min_max, METH_NOARGS, - MCRF_METHOD(HeightMap, min_max, - MCRF_SIG("()", "tuple[float, float]"), - MCRF_DESC("Get the minimum and maximum height values in the map."), - MCRF_RETURNS("tuple[float, float]: (min_value, max_value)") - )}, - {"count_in_range", (PyCFunction)PyHeightMap::count_in_range, METH_VARARGS, - MCRF_METHOD(HeightMap, count_in_range, - MCRF_SIG("(range: tuple[float, float])", "int"), - MCRF_DESC("Count cells with values in the specified range (inclusive)."), - MCRF_ARGS_START - MCRF_ARG("range", "Value range as (min, max) tuple") - MCRF_RETURNS("int: Number of cells with values in range") - )}, {NULL} }; @@ -350,191 +300,3 @@ PyObject* PyHeightMap::normalize(PyHeightMapObject* self, PyObject* args, PyObje Py_INCREF(self); return (PyObject*)self; } - -// Query methods (#196) - -// Method: get(pos) -> float -PyObject* PyHeightMap::get(PyHeightMapObject* self, PyObject* args) -{ - PyObject* pos_obj = nullptr; - if (!PyArg_ParseTuple(args, "O", &pos_obj)) { - return nullptr; - } - - if (!self->heightmap) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized"); - return nullptr; - } - - // Parse position tuple - if (!PyTuple_Check(pos_obj) || PyTuple_Size(pos_obj) != 2) { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y)"); - return nullptr; - } - - int x = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 0)); - int y = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 1)); - - if (PyErr_Occurred()) { - return nullptr; - } - - // Bounds check - if (x < 0 || x >= self->heightmap->w || y < 0 || y >= self->heightmap->h) { - PyErr_Format(PyExc_IndexError, - "Position (%d, %d) out of bounds for HeightMap of size (%d, %d)", - x, y, self->heightmap->w, self->heightmap->h); - return nullptr; - } - - float value = TCOD_heightmap_get_value(self->heightmap, x, y); - return PyFloat_FromDouble(value); -} - -// Method: get_interpolated(pos) -> float -PyObject* PyHeightMap::get_interpolated(PyHeightMapObject* self, PyObject* args) -{ - PyObject* pos_obj = nullptr; - if (!PyArg_ParseTuple(args, "O", &pos_obj)) { - return nullptr; - } - - if (!self->heightmap) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized"); - return nullptr; - } - - // Parse position tuple (floats) - if (!PyTuple_Check(pos_obj) || PyTuple_Size(pos_obj) != 2) { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y)"); - return nullptr; - } - - float x = (float)PyFloat_AsDouble(PyTuple_GetItem(pos_obj, 0)); - float y = (float)PyFloat_AsDouble(PyTuple_GetItem(pos_obj, 1)); - - if (PyErr_Occurred()) { - return nullptr; - } - - float value = TCOD_heightmap_get_interpolated_value(self->heightmap, x, y); - return PyFloat_FromDouble(value); -} - -// Method: get_slope(pos) -> float -PyObject* PyHeightMap::get_slope(PyHeightMapObject* self, PyObject* args) -{ - PyObject* pos_obj = nullptr; - if (!PyArg_ParseTuple(args, "O", &pos_obj)) { - return nullptr; - } - - if (!self->heightmap) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized"); - return nullptr; - } - - // Parse position tuple - if (!PyTuple_Check(pos_obj) || PyTuple_Size(pos_obj) != 2) { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y)"); - return nullptr; - } - - int x = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 0)); - int y = (int)PyLong_AsLong(PyTuple_GetItem(pos_obj, 1)); - - if (PyErr_Occurred()) { - return nullptr; - } - - // Bounds check - if (x < 0 || x >= self->heightmap->w || y < 0 || y >= self->heightmap->h) { - PyErr_Format(PyExc_IndexError, - "Position (%d, %d) out of bounds for HeightMap of size (%d, %d)", - x, y, self->heightmap->w, self->heightmap->h); - return nullptr; - } - - float slope = TCOD_heightmap_get_slope(self->heightmap, x, y); - return PyFloat_FromDouble(slope); -} - -// Method: get_normal(pos, water_level=0.0) -> tuple[float, float, float] -PyObject* PyHeightMap::get_normal(PyHeightMapObject* self, PyObject* args, PyObject* kwds) -{ - static const char* keywords[] = {"pos", "water_level", nullptr}; - PyObject* pos_obj = nullptr; - float water_level = 0.0f; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast(keywords), - &pos_obj, &water_level)) { - return nullptr; - } - - if (!self->heightmap) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized"); - return nullptr; - } - - // Parse position tuple (floats) - if (!PyTuple_Check(pos_obj) || PyTuple_Size(pos_obj) != 2) { - PyErr_SetString(PyExc_TypeError, "pos must be a tuple of (x, y)"); - return nullptr; - } - - float x = (float)PyFloat_AsDouble(PyTuple_GetItem(pos_obj, 0)); - float y = (float)PyFloat_AsDouble(PyTuple_GetItem(pos_obj, 1)); - - if (PyErr_Occurred()) { - return nullptr; - } - - float n[3]; - TCOD_heightmap_get_normal(self->heightmap, x, y, n, water_level); - - return Py_BuildValue("(fff)", n[0], n[1], n[2]); -} - -// Method: min_max() -> tuple[float, float] -PyObject* PyHeightMap::min_max(PyHeightMapObject* self, PyObject* Py_UNUSED(args)) -{ - if (!self->heightmap) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized"); - return nullptr; - } - - float min_val, max_val; - TCOD_heightmap_get_minmax(self->heightmap, &min_val, &max_val); - - return Py_BuildValue("(ff)", min_val, max_val); -} - -// Method: count_in_range(range) -> int -PyObject* PyHeightMap::count_in_range(PyHeightMapObject* self, PyObject* args) -{ - PyObject* range_obj = nullptr; - if (!PyArg_ParseTuple(args, "O", &range_obj)) { - return nullptr; - } - - if (!self->heightmap) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized"); - return nullptr; - } - - // Parse range tuple - if (!PyTuple_Check(range_obj) || PyTuple_Size(range_obj) != 2) { - PyErr_SetString(PyExc_TypeError, "range must be a tuple of (min, max)"); - return nullptr; - } - - float min_val = (float)PyFloat_AsDouble(PyTuple_GetItem(range_obj, 0)); - float max_val = (float)PyFloat_AsDouble(PyTuple_GetItem(range_obj, 1)); - - if (PyErr_Occurred()) { - return nullptr; - } - - int count = TCOD_heightmap_count_cells(self->heightmap, min_val, max_val); - return PyLong_FromLong(count); -} diff --git a/src/PyHeightMap.h b/src/PyHeightMap.h index cd33809..df02af6 100644 --- a/src/PyHeightMap.h +++ b/src/PyHeightMap.h @@ -32,14 +32,6 @@ public: static PyObject* clamp(PyHeightMapObject* self, PyObject* args, PyObject* kwds); static PyObject* normalize(PyHeightMapObject* self, PyObject* args, PyObject* kwds); - // Query methods (#196) - static PyObject* get(PyHeightMapObject* self, PyObject* args); - static PyObject* get_interpolated(PyHeightMapObject* self, PyObject* args); - static PyObject* get_slope(PyHeightMapObject* self, PyObject* args); - static PyObject* get_normal(PyHeightMapObject* self, PyObject* args, PyObject* kwds); - static PyObject* min_max(PyHeightMapObject* self, PyObject* Py_UNUSED(args)); - static PyObject* count_in_range(PyHeightMapObject* self, PyObject* args); - // Method and property definitions static PyMethodDef methods[]; static PyGetSetDef getsetters[]; diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 2f4ae50..cff7649 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -9,7 +9,6 @@ #include "PyFOV.h" #include "PyPositionHelper.h" // For standardized position argument parsing #include "PyVector.h" // #179, #181 - For Vector return types -#include "PyHeightMap.h" // #199 - HeightMap application methods #include #include // #142 - for std::floor, std::isnan #include // #150 - for strcmp @@ -1691,229 +1690,6 @@ PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) { Py_RETURN_NONE; } -// #199 - HeightMap application methods - -PyObject* UIGrid::py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = {"source", "range", "walkable", "transparent", nullptr}; - PyObject* source_obj = nullptr; - PyObject* range_obj = nullptr; - PyObject* walkable_obj = Py_None; - PyObject* transparent_obj = Py_None; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", const_cast(keywords), - &source_obj, &range_obj, &walkable_obj, &transparent_obj)) { - return nullptr; - } - - // Validate source is a HeightMap - PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap"); - if (!heightmap_type) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found in module"); - return nullptr; - } - bool is_heightmap = PyObject_IsInstance(source_obj, heightmap_type); - Py_DECREF(heightmap_type); - if (!is_heightmap) { - PyErr_SetString(PyExc_TypeError, "source must be a HeightMap"); - return nullptr; - } - PyHeightMapObject* hmap = (PyHeightMapObject*)source_obj; - - if (!hmap->heightmap) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized"); - return nullptr; - } - - // Parse range tuple - if (!PyTuple_Check(range_obj) || PyTuple_Size(range_obj) != 2) { - PyErr_SetString(PyExc_TypeError, "range must be a tuple of (min, max)"); - return nullptr; - } - - float range_min = (float)PyFloat_AsDouble(PyTuple_GetItem(range_obj, 0)); - float range_max = (float)PyFloat_AsDouble(PyTuple_GetItem(range_obj, 1)); - - if (PyErr_Occurred()) { - return nullptr; - } - - // Check size match - if (hmap->heightmap->w != self->data->grid_w || hmap->heightmap->h != self->data->grid_h) { - PyErr_Format(PyExc_ValueError, - "HeightMap size (%d, %d) does not match Grid size (%d, %d)", - hmap->heightmap->w, hmap->heightmap->h, self->data->grid_w, self->data->grid_h); - return nullptr; - } - - // Parse optional walkable/transparent booleans - bool set_walkable = (walkable_obj != Py_None); - bool set_transparent = (transparent_obj != Py_None); - bool walkable_value = false; - bool transparent_value = false; - - if (set_walkable) { - walkable_value = PyObject_IsTrue(walkable_obj); - } - if (set_transparent) { - transparent_value = PyObject_IsTrue(transparent_obj); - } - - // Apply threshold - for (int y = 0; y < self->data->grid_h; y++) { - for (int x = 0; x < self->data->grid_w; x++) { - float value = TCOD_heightmap_get_value(hmap->heightmap, x, y); - if (value >= range_min && value <= range_max) { - UIGridPoint& point = self->data->at(x, y); - if (set_walkable) { - point.walkable = walkable_value; - } - if (set_transparent) { - point.transparent = transparent_value; - } - } - } - } - - // Sync TCOD map if it exists - if (self->data->getTCODMap()) { - self->data->syncTCODMap(); - } - - // Return self for chaining - Py_INCREF(self); - return (PyObject*)self; -} - -PyObject* UIGrid::py_apply_ranges(PyUIGridObject* self, PyObject* args) { - PyObject* source_obj = nullptr; - PyObject* ranges_obj = nullptr; - - if (!PyArg_ParseTuple(args, "OO", &source_obj, &ranges_obj)) { - return nullptr; - } - - // Validate source is a HeightMap - PyObject* heightmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap"); - if (!heightmap_type) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found in module"); - return nullptr; - } - bool is_heightmap = PyObject_IsInstance(source_obj, heightmap_type); - Py_DECREF(heightmap_type); - if (!is_heightmap) { - PyErr_SetString(PyExc_TypeError, "source must be a HeightMap"); - return nullptr; - } - PyHeightMapObject* hmap = (PyHeightMapObject*)source_obj; - - if (!hmap->heightmap) { - PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized"); - return nullptr; - } - - // Validate ranges is a list - if (!PyList_Check(ranges_obj)) { - PyErr_SetString(PyExc_TypeError, "ranges must be a list"); - return nullptr; - } - - // Check size match - if (hmap->heightmap->w != self->data->grid_w || hmap->heightmap->h != self->data->grid_h) { - PyErr_Format(PyExc_ValueError, - "HeightMap size (%d, %d) does not match Grid size (%d, %d)", - hmap->heightmap->w, hmap->heightmap->h, self->data->grid_w, self->data->grid_h); - return nullptr; - } - - // Parse all ranges first to catch errors early - struct RangeEntry { - float min, max; - bool set_walkable, set_transparent; - bool walkable_value, transparent_value; - }; - std::vector entries; - - Py_ssize_t num_ranges = PyList_Size(ranges_obj); - for (Py_ssize_t i = 0; i < num_ranges; i++) { - PyObject* entry = PyList_GetItem(ranges_obj, i); - - if (!PyTuple_Check(entry) || PyTuple_Size(entry) != 2) { - PyErr_Format(PyExc_TypeError, - "ranges[%zd] must be a tuple of (range, properties_dict)", i); - return nullptr; - } - - PyObject* range_tuple = PyTuple_GetItem(entry, 0); - PyObject* props_dict = PyTuple_GetItem(entry, 1); - - if (!PyTuple_Check(range_tuple) || PyTuple_Size(range_tuple) != 2) { - PyErr_Format(PyExc_TypeError, - "ranges[%zd] range must be a tuple of (min, max)", i); - return nullptr; - } - - if (!PyDict_Check(props_dict)) { - PyErr_Format(PyExc_TypeError, - "ranges[%zd] properties must be a dict", i); - return nullptr; - } - - RangeEntry re; - re.min = (float)PyFloat_AsDouble(PyTuple_GetItem(range_tuple, 0)); - re.max = (float)PyFloat_AsDouble(PyTuple_GetItem(range_tuple, 1)); - - if (PyErr_Occurred()) { - return nullptr; - } - - // Parse walkable from dict - PyObject* walkable_val = PyDict_GetItemString(props_dict, "walkable"); - re.set_walkable = (walkable_val != nullptr); - if (re.set_walkable) { - re.walkable_value = PyObject_IsTrue(walkable_val); - } - - // Parse transparent from dict - PyObject* transparent_val = PyDict_GetItemString(props_dict, "transparent"); - re.set_transparent = (transparent_val != nullptr); - if (re.set_transparent) { - re.transparent_value = PyObject_IsTrue(transparent_val); - } - - entries.push_back(re); - } - - // Apply all ranges in a single pass - for (int y = 0; y < self->data->grid_h; y++) { - for (int x = 0; x < self->data->grid_w; x++) { - float value = TCOD_heightmap_get_value(hmap->heightmap, x, y); - UIGridPoint& point = self->data->at(x, y); - - // Check each range (first match wins) - for (const auto& re : entries) { - if (value >= re.min && value <= re.max) { - if (re.set_walkable) { - point.walkable = re.walkable_value; - } - if (re.set_transparent) { - point.transparent = re.transparent_value; - } - break; // First matching range wins - } - } - } - } - - // Sync TCOD map if it exists - if (self->data->getTCODMap()) { - self->data->syncTCODMap(); - } - - // Return self for chaining - Py_INCREF(self); - return (PyObject*)self; -} - PyMethodDef UIGrid::methods[] = { {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, @@ -1983,35 +1759,6 @@ PyMethodDef UIGrid::methods[] = { " grid.center_camera() # Center on middle of grid\n" " grid.center_camera((5, 10)) # Center on tile (5, 10)\n" " grid.center_camera((0, 0)) # Center on tile (0, 0)"}, - // #199 - HeightMap application methods - {"apply_threshold", (PyCFunction)UIGrid::py_apply_threshold, METH_VARARGS | METH_KEYWORDS, - "apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> Grid\n\n" - "Apply walkable/transparent properties where heightmap values are in range.\n\n" - "Args:\n" - " source: HeightMap with values to check. Must match grid size.\n" - " range: Tuple of (min, max) - cells with values in this range are affected.\n" - " walkable: If not None, set walkable to this value for cells in range.\n" - " transparent: If not None, set transparent to this value for cells in range.\n\n" - "Returns:\n" - " Grid: self, for method chaining.\n\n" - "Raises:\n" - " ValueError: If HeightMap size doesn't match grid size."}, - {"apply_ranges", (PyCFunction)UIGrid::py_apply_ranges, METH_VARARGS, - "apply_ranges(source: HeightMap, ranges: list) -> Grid\n\n" - "Apply multiple thresholds in a single pass.\n\n" - "Args:\n" - " source: HeightMap with values to check. Must match grid size.\n" - " ranges: List of (range_tuple, properties_dict) tuples.\n" - " range_tuple: (min, max) value range\n" - " properties_dict: {'walkable': bool, 'transparent': bool}\n\n" - "Returns:\n" - " Grid: self, for method chaining.\n\n" - "Example:\n" - " grid.apply_ranges(terrain, [\n" - " ((0.0, 0.3), {'walkable': False, 'transparent': True}), # Water\n" - " ((0.3, 0.8), {'walkable': True, 'transparent': True}), # Land\n" - " ((0.8, 1.0), {'walkable': False, 'transparent': False}), # Mountains\n" - " ])"}, {NULL, NULL, 0, NULL} }; @@ -2104,35 +1851,6 @@ PyMethodDef UIGrid_all_methods[] = { " grid.center_camera() # Center on middle of grid\n" " grid.center_camera((5, 10)) # Center on tile (5, 10)\n" " grid.center_camera((0, 0)) # Center on tile (0, 0)"}, - // #199 - HeightMap application methods - {"apply_threshold", (PyCFunction)UIGrid::py_apply_threshold, METH_VARARGS | METH_KEYWORDS, - "apply_threshold(source: HeightMap, range: tuple, walkable: bool = None, transparent: bool = None) -> Grid\n\n" - "Apply walkable/transparent properties where heightmap values are in range.\n\n" - "Args:\n" - " source: HeightMap with values to check. Must match grid size.\n" - " range: Tuple of (min, max) - cells with values in this range are affected.\n" - " walkable: If not None, set walkable to this value for cells in range.\n" - " transparent: If not None, set transparent to this value for cells in range.\n\n" - "Returns:\n" - " Grid: self, for method chaining.\n\n" - "Raises:\n" - " ValueError: If HeightMap size doesn't match grid size."}, - {"apply_ranges", (PyCFunction)UIGrid::py_apply_ranges, METH_VARARGS, - "apply_ranges(source: HeightMap, ranges: list) -> Grid\n\n" - "Apply multiple thresholds in a single pass.\n\n" - "Args:\n" - " source: HeightMap with values to check. Must match grid size.\n" - " ranges: List of (range_tuple, properties_dict) tuples.\n" - " range_tuple: (min, max) value range\n" - " properties_dict: {'walkable': bool, 'transparent': bool}\n\n" - "Returns:\n" - " Grid: self, for method chaining.\n\n" - "Example:\n" - " grid.apply_ranges(terrain, [\n" - " ((0.0, 0.3), {'walkable': False, 'transparent': True}), # Water\n" - " ((0.3, 0.8), {'walkable': True, 'transparent': True}), # Land\n" - " ((0.8, 1.0), {'walkable': False, 'transparent': False}), # Mountains\n" - " ])"}, {NULL} // Sentinel }; diff --git a/src/UIGrid.h b/src/UIGrid.h index aa1336b..3d1a56a 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -179,10 +179,6 @@ public: static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115 static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169 - // #199 - HeightMap application methods - static PyObject* py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds); - static PyObject* py_apply_ranges(PyUIGridObject* self, PyObject* args); - // #169 - Camera positioning void center_camera(); // Center on grid's middle tile void center_camera(float tile_x, float tile_y); // Center on specific tile diff --git a/tests/unit/test_grid_apply.py b/tests/unit/test_grid_apply.py deleted file mode 100644 index 65a365a..0000000 --- a/tests/unit/test_grid_apply.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 -"""Unit tests for Grid.apply_threshold and Grid.apply_ranges (#199) - -Tests the Grid methods for applying HeightMap data to walkable/transparent properties. -""" - -import sys -import mcrfpy - - -def test_apply_threshold_walkable(): - """apply_threshold sets walkable property correctly""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - # All cells start with default walkable - grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True) - - # Check a few cells - assert grid.at((5, 5)).walkable == True - assert grid.at((0, 0)).walkable == True - assert grid.at((9, 9)).walkable == True - print("PASS: test_apply_threshold_walkable") - - -def test_apply_threshold_transparent(): - """apply_threshold sets transparent property correctly""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - grid.apply_threshold(hmap, range=(0.0, 1.0), transparent=False) - - assert grid.at((5, 5)).transparent == False - print("PASS: test_apply_threshold_transparent") - - -def test_apply_threshold_both(): - """apply_threshold sets both walkable and transparent""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True, transparent=True) - - point = grid.at((5, 5)) - assert point.walkable == True - assert point.transparent == True - print("PASS: test_apply_threshold_both") - - -def test_apply_threshold_out_of_range(): - """apply_threshold doesn't affect cells outside range""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - # Set initial state - grid.at((5, 5)).walkable = False - grid.at((5, 5)).transparent = False - - # Apply threshold with range that excludes 0.5 - grid.apply_threshold(hmap, range=(0.0, 0.4), walkable=True, transparent=True) - - # Cell should remain unchanged - assert grid.at((5, 5)).walkable == False - assert grid.at((5, 5)).transparent == False - print("PASS: test_apply_threshold_out_of_range") - - -def test_apply_threshold_returns_self(): - """apply_threshold returns self for chaining""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - result = grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True) - assert result is grid, "apply_threshold should return self" - print("PASS: test_apply_threshold_returns_self") - - -def test_apply_threshold_size_mismatch(): - """apply_threshold raises ValueError for size mismatch""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((20, 20), fill=0.5) - - try: - grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=True) - print("FAIL: test_apply_threshold_size_mismatch - should have raised ValueError") - sys.exit(1) - except ValueError as e: - assert "size" in str(e).lower() - - print("PASS: test_apply_threshold_size_mismatch") - - -def test_apply_threshold_invalid_source(): - """apply_threshold raises TypeError for non-HeightMap source""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - - try: - grid.apply_threshold("not a heightmap", range=(0.0, 1.0), walkable=True) - print("FAIL: test_apply_threshold_invalid_source - should have raised TypeError") - sys.exit(1) - except TypeError: - pass - - print("PASS: test_apply_threshold_invalid_source") - - -def test_apply_threshold_none_values(): - """apply_threshold with None values leaves properties unchanged""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - # Set initial state - grid.at((5, 5)).walkable = True - grid.at((5, 5)).transparent = False - - # Apply with only walkable=False, transparent should stay unchanged - grid.apply_threshold(hmap, range=(0.0, 1.0), walkable=False) - - assert grid.at((5, 5)).walkable == False - assert grid.at((5, 5)).transparent == False # Unchanged - print("PASS: test_apply_threshold_none_values") - - -def test_apply_ranges_basic(): - """apply_ranges applies multiple ranges correctly""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - # Apply a range that covers 0.5 - grid.apply_ranges(hmap, [ - ((0.4, 0.6), {"walkable": True, "transparent": True}), - ]) - - assert grid.at((5, 5)).walkable == True - assert grid.at((5, 5)).transparent == True - print("PASS: test_apply_ranges_basic") - - -def test_apply_ranges_first_match_wins(): - """apply_ranges uses first matching range""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - # Both ranges cover 0.5, first should win - grid.apply_ranges(hmap, [ - ((0.0, 0.6), {"walkable": True}), - ((0.4, 1.0), {"walkable": False}), - ]) - - assert grid.at((5, 5)).walkable == True # First match wins - print("PASS: test_apply_ranges_first_match_wins") - - -def test_apply_ranges_returns_self(): - """apply_ranges returns self for chaining""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - result = grid.apply_ranges(hmap, [ - ((0.0, 1.0), {"walkable": True}), - ]) - assert result is grid, "apply_ranges should return self" - print("PASS: test_apply_ranges_returns_self") - - -def test_apply_ranges_size_mismatch(): - """apply_ranges raises ValueError for size mismatch""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((5, 5), fill=0.5) - - try: - grid.apply_ranges(hmap, [ - ((0.0, 1.0), {"walkable": True}), - ]) - print("FAIL: test_apply_ranges_size_mismatch - should have raised ValueError") - sys.exit(1) - except ValueError as e: - assert "size" in str(e).lower() - - print("PASS: test_apply_ranges_size_mismatch") - - -def test_apply_ranges_empty_list(): - """apply_ranges with empty list doesn't change anything""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - grid.at((5, 5)).walkable = True - grid.at((5, 5)).transparent = False - - grid.apply_ranges(hmap, []) - - # Should remain unchanged - assert grid.at((5, 5)).walkable == True - assert grid.at((5, 5)).transparent == False - print("PASS: test_apply_ranges_empty_list") - - -def test_apply_ranges_no_match(): - """apply_ranges leaves cells unchanged when no range matches""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - grid.at((5, 5)).walkable = True - grid.at((5, 5)).transparent = True - - # Ranges that don't include 0.5 - grid.apply_ranges(hmap, [ - ((0.0, 0.4), {"walkable": False}), - ((0.6, 1.0), {"transparent": False}), - ]) - - # Should remain unchanged - assert grid.at((5, 5)).walkable == True - assert grid.at((5, 5)).transparent == True - print("PASS: test_apply_ranges_no_match") - - -def test_apply_ranges_invalid_format(): - """apply_ranges raises TypeError for invalid format""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - - # Invalid: not a list - try: - grid.apply_ranges(hmap, "not a list") - print("FAIL: should have raised TypeError for non-list") - sys.exit(1) - except TypeError: - pass - - # Invalid: entry not a tuple - try: - grid.apply_ranges(hmap, ["not a tuple"]) - print("FAIL: should have raised TypeError for non-tuple entry") - sys.exit(1) - except TypeError: - pass - - # Invalid: range not a tuple - try: - grid.apply_ranges(hmap, [ - ([0.0, 1.0], {"walkable": True}), # list instead of tuple for range - ]) - print("FAIL: should have raised TypeError for non-tuple range") - sys.exit(1) - except TypeError: - pass - - # Invalid: props not a dict - try: - grid.apply_ranges(hmap, [ - ((0.0, 1.0), "not a dict"), - ]) - print("FAIL: should have raised TypeError for non-dict props") - sys.exit(1) - except TypeError: - pass - - print("PASS: test_apply_ranges_invalid_format") - - -def test_chaining(): - """Methods can be chained together""" - grid = mcrfpy.Grid(grid_size=(10, 10)) - hmap = mcrfpy.HeightMap((10, 10)) - - # Chain multiple operations - hmap.fill(0.5) - - result = (grid - .apply_threshold(hmap, range=(0.0, 0.4), walkable=False) - .apply_threshold(hmap, range=(0.6, 1.0), transparent=False) - .apply_ranges(hmap, [ - ((0.4, 0.6), {"walkable": True, "transparent": True}), - ])) - - assert result is grid - print("PASS: test_chaining") - - -def run_all_tests(): - """Run all tests""" - print("Running Grid apply method tests...") - print() - - test_apply_threshold_walkable() - test_apply_threshold_transparent() - test_apply_threshold_both() - test_apply_threshold_out_of_range() - test_apply_threshold_returns_self() - test_apply_threshold_size_mismatch() - test_apply_threshold_invalid_source() - test_apply_threshold_none_values() - test_apply_ranges_basic() - test_apply_ranges_first_match_wins() - test_apply_ranges_returns_self() - test_apply_ranges_size_mismatch() - test_apply_ranges_empty_list() - test_apply_ranges_no_match() - test_apply_ranges_invalid_format() - test_chaining() - - print() - print("All Grid apply method tests PASSED!") - - -# Run tests directly -run_all_tests() -sys.exit(0) diff --git a/tests/unit/test_heightmap_query.py b/tests/unit/test_heightmap_query.py deleted file mode 100644 index 2522753..0000000 --- a/tests/unit/test_heightmap_query.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env python3 -"""Unit tests for mcrfpy.HeightMap query methods (#196) - -Tests the HeightMap query methods: get, get_interpolated, get_slope, get_normal, min_max, count_in_range -""" - -import sys -import math -import mcrfpy - - -def test_get_basic(): - """get() returns correct value at position""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - value = hmap.get((5, 5)) - assert abs(value - 0.5) < 0.001, f"Expected 0.5, got {value}" - print("PASS: test_get_basic") - - -def test_get_corners(): - """get() works at all corners""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.25) - - # All corners should have the fill value - assert abs(hmap.get((0, 0)) - 0.25) < 0.001 - assert abs(hmap.get((9, 0)) - 0.25) < 0.001 - assert abs(hmap.get((0, 9)) - 0.25) < 0.001 - assert abs(hmap.get((9, 9)) - 0.25) < 0.001 - print("PASS: test_get_corners") - - -def test_get_out_of_bounds(): - """get() raises IndexError for out-of-bounds position""" - hmap = mcrfpy.HeightMap((10, 10)) - - # Test various out-of-bounds positions - for pos in [(-1, 0), (0, -1), (10, 0), (0, 10), (10, 10)]: - try: - hmap.get(pos) - print(f"FAIL: test_get_out_of_bounds - should have raised IndexError for {pos}") - sys.exit(1) - except IndexError: - pass - - print("PASS: test_get_out_of_bounds") - - -def test_get_invalid_type(): - """get() raises TypeError for invalid position""" - hmap = mcrfpy.HeightMap((10, 10)) - - try: - hmap.get([5, 5]) # list instead of tuple - print("FAIL: test_get_invalid_type - should have raised TypeError") - sys.exit(1) - except TypeError: - pass - - print("PASS: test_get_invalid_type") - - -def test_get_interpolated_basic(): - """get_interpolated() returns value at float position""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - value = hmap.get_interpolated((5.5, 5.5)) - # With uniform fill, interpolation should return same value - assert abs(value - 0.5) < 0.001, f"Expected ~0.5, got {value}" - print("PASS: test_get_interpolated_basic") - - -def test_get_interpolated_at_integers(): - """get_interpolated() matches get() at integer positions""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.75) - - int_value = hmap.get((3, 4)) - interp_value = hmap.get_interpolated((3.0, 4.0)) - - assert abs(int_value - interp_value) < 0.001, f"Values differ: {int_value} vs {interp_value}" - print("PASS: test_get_interpolated_at_integers") - - -def test_get_slope_flat(): - """get_slope() returns 0 for flat terrain""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - slope = hmap.get_slope((5, 5)) - # Flat terrain should have slope near 0 - assert abs(slope) < 0.01, f"Expected ~0 for flat terrain, got {slope}" - print("PASS: test_get_slope_flat") - - -def test_get_slope_out_of_bounds(): - """get_slope() raises IndexError for out-of-bounds position""" - hmap = mcrfpy.HeightMap((10, 10)) - - try: - hmap.get_slope((10, 5)) - print("FAIL: test_get_slope_out_of_bounds - should have raised IndexError") - sys.exit(1) - except IndexError: - pass - - print("PASS: test_get_slope_out_of_bounds") - - -def test_get_normal_flat(): - """get_normal() returns up vector for flat terrain""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - nx, ny, nz = hmap.get_normal((5.0, 5.0)) - - # Flat terrain should have normal pointing up (0, 0, 1) - assert abs(nx) < 0.01, f"Expected nx~0, got {nx}" - assert abs(ny) < 0.01, f"Expected ny~0, got {ny}" - assert abs(nz - 1.0) < 0.01, f"Expected nz~1, got {nz}" - print("PASS: test_get_normal_flat") - - -def test_get_normal_with_water_level(): - """get_normal() accepts water_level parameter""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - nx, ny, nz = hmap.get_normal((5.0, 5.0), water_level=0.3) - - # Should still return valid normal - assert isinstance(nx, float) - assert isinstance(ny, float) - assert isinstance(nz, float) - print("PASS: test_get_normal_with_water_level") - - -def test_min_max_uniform(): - """min_max() returns correct values for uniform heightmap""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - min_val, max_val = hmap.min_max() - - assert abs(min_val - 0.5) < 0.001, f"Expected min=0.5, got {min_val}" - assert abs(max_val - 0.5) < 0.001, f"Expected max=0.5, got {max_val}" - print("PASS: test_min_max_uniform") - - -def test_min_max_after_operations(): - """min_max() updates after operations""" - hmap = mcrfpy.HeightMap((10, 10)) - hmap.fill(0.0).add_constant(0.5).scale(2.0) - - min_val, max_val = hmap.min_max() - expected = 1.0 # 0.0 + 0.5 * 2.0 - - assert abs(min_val - expected) < 0.001, f"Expected min={expected}, got {min_val}" - assert abs(max_val - expected) < 0.001, f"Expected max={expected}, got {max_val}" - print("PASS: test_min_max_after_operations") - - -def test_count_in_range_all(): - """count_in_range() returns all cells for uniform map in range""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - count = hmap.count_in_range((0.0, 1.0)) - - assert count == 100, f"Expected 100 cells, got {count}" - print("PASS: test_count_in_range_all") - - -def test_count_in_range_none(): - """count_in_range() returns 0 when no cells in range""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - count = hmap.count_in_range((0.0, 0.4)) - - assert count == 0, f"Expected 0 cells, got {count}" - print("PASS: test_count_in_range_none") - - -def test_count_in_range_exact(): - """count_in_range() with exact bounds""" - hmap = mcrfpy.HeightMap((10, 10), fill=0.5) - count = hmap.count_in_range((0.5, 0.5)) - - # Should count all cells since fill value is exactly 0.5 - assert count == 100, f"Expected 100 cells at exact value, got {count}" - print("PASS: test_count_in_range_exact") - - -def test_count_in_range_invalid(): - """count_in_range() raises TypeError for invalid range""" - hmap = mcrfpy.HeightMap((10, 10)) - - try: - hmap.count_in_range([0.0, 1.0]) # list instead of tuple - print("FAIL: test_count_in_range_invalid - should have raised TypeError") - sys.exit(1) - except TypeError: - pass - - print("PASS: test_count_in_range_invalid") - - -def run_all_tests(): - """Run all tests""" - print("Running HeightMap query method tests...") - print() - - test_get_basic() - test_get_corners() - test_get_out_of_bounds() - test_get_invalid_type() - test_get_interpolated_basic() - test_get_interpolated_at_integers() - test_get_slope_flat() - test_get_slope_out_of_bounds() - test_get_normal_flat() - test_get_normal_with_water_level() - test_min_max_uniform() - test_min_max_after_operations() - test_count_in_range_all() - test_count_in_range_none() - test_count_in_range_exact() - test_count_in_range_invalid() - - print() - print("All HeightMap query method tests PASSED!") - - -# Run tests directly -run_all_tests() -sys.exit(0)