Grid: add apply_threshold and apply_ranges for HeightMap (closes #199)

Add methods to apply HeightMap data to Grid walkable/transparent properties:
- apply_threshold(source, range, walkable, transparent): Apply properties
  to cells where HeightMap value is in the specified range
- apply_ranges(source, ranges): Apply multiple threshold rules in one pass

Features:
- Size mismatch between HeightMap and Grid raises ValueError
- Both methods return self for chaining
- Uses dynamic type lookup via module for HeightMap type checking
- First matching range wins in apply_ranges
- Cells not matching any range remain unchanged
- TCOD map is synced after changes for FOV/pathfinding

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-01-11 20:42:47 -05:00
commit bf8557798a
3 changed files with 596 additions and 0 deletions

View file

@ -9,6 +9,7 @@
#include "PyFOV.h" #include "PyFOV.h"
#include "PyPositionHelper.h" // For standardized position argument parsing #include "PyPositionHelper.h" // For standardized position argument parsing
#include "PyVector.h" // #179, #181 - For Vector return types #include "PyVector.h" // #179, #181 - For Vector return types
#include "PyHeightMap.h" // #199 - HeightMap application methods
#include <algorithm> #include <algorithm>
#include <cmath> // #142 - for std::floor, std::isnan #include <cmath> // #142 - for std::floor, std::isnan
#include <cstring> // #150 - for strcmp #include <cstring> // #150 - for strcmp
@ -1690,6 +1691,229 @@ PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) {
Py_RETURN_NONE; 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<char**>(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<RangeEntry> 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[] = { PyMethodDef UIGrid::methods[] = {
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
@ -1759,6 +1983,35 @@ PyMethodDef UIGrid::methods[] = {
" grid.center_camera() # Center on middle of grid\n" " grid.center_camera() # Center on middle of grid\n"
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n" " grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
" grid.center_camera((0, 0)) # Center on tile (0, 0)"}, " 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} {NULL, NULL, 0, NULL}
}; };
@ -1851,6 +2104,35 @@ PyMethodDef UIGrid_all_methods[] = {
" grid.center_camera() # Center on middle of grid\n" " grid.center_camera() # Center on middle of grid\n"
" grid.center_camera((5, 10)) # Center on tile (5, 10)\n" " grid.center_camera((5, 10)) # Center on tile (5, 10)\n"
" grid.center_camera((0, 0)) # Center on tile (0, 0)"}, " 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 {NULL} // Sentinel
}; };

View file

@ -179,6 +179,10 @@ public:
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
// #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 // #169 - Camera positioning
void center_camera(); // Center on grid's middle tile void center_camera(); // Center on grid's middle tile
void center_camera(float tile_x, float tile_y); // Center on specific tile void center_camera(float tile_x, float tile_y); // Center on specific tile

View file

@ -0,0 +1,310 @@
#!/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)