HeightMap: core class with scalar operations (closes #193)
Implement the foundational HeightMap class for procedural generation: - HeightMap(size, fill=0.0) constructor with libtcod backend - Immutable size property after construction - Scalar operations returning self for method chaining: - fill(value), clear() - add_constant(value), scale(factor) - clamp(min=0.0, max=1.0), normalize(min=0.0, max=1.0) Includes procedural generation spec document and unit tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b32f5af28c
commit
c095be4b73
5 changed files with 1586 additions and 1 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
|
|
@ -21,6 +21,7 @@
|
||||||
#include "PyKeyboard.h"
|
#include "PyKeyboard.h"
|
||||||
#include "PyMouse.h"
|
#include "PyMouse.h"
|
||||||
#include "UIGridPathfinding.h" // AStarPath and DijkstraMap types
|
#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"
|
||||||
|
|
@ -415,6 +416,9 @@ PyObject* PyInit_mcrfpy()
|
||||||
&mcrfpydef::PyAStarPathType,
|
&mcrfpydef::PyAStarPathType,
|
||||||
&mcrfpydef::PyDijkstraMapType,
|
&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)
|
||||||
|
|
@ -440,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);
|
||||||
|
|
|
||||||
285
src/PyHeightMap.cpp
Normal file
285
src/PyHeightMap.cpp
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
199
tests/unit/test_heightmap_basic.py
Normal file
199
tests/unit/test_heightmap_basic.py
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
#!/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 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()
|
||||||
|
|
||||||
|
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