diff --git a/src/MapOps.h b/src/MapOps.h new file mode 100644 index 0000000..8ed045f --- /dev/null +++ b/src/MapOps.h @@ -0,0 +1,470 @@ +#pragma once +#include "Python.h" +#include +#include + +// ============================================================================ +// MapOps - Template abstractions for 2D grid map operations +// ============================================================================ +// +// Provides common operations for HeightMap (float) and DiscreteMap (uint8_t). +// Uses policy-based design for type-specific behavior (clamping, conversion). +// +// Benefits: +// - Single implementation for fill, copy, region iteration +// - Type-appropriate clamping via saturation policies +// - Compile-time polymorphism (no virtual overhead) +// - Shared region parameter parsing from Python kwargs +// ============================================================================ + +// Forward declarations +class PyPositionHelper; + +// ============================================================================ +// Unified region struct for all map operations +// ============================================================================ + +struct MapRegion { + // Validated region coordinates + int dest_x, dest_y; // Destination origin in target map + int src_x, src_y; // Source origin (for binary ops, 0 for scalar ops) + int width, height; // Region dimensions + + // Full map dimensions (for iteration) + int dest_w, dest_h; + int src_w, src_h; + + // Direct indexing helpers + inline int dest_idx(int x, int y) const { + return (dest_y + y) * dest_w + (dest_x + x); + } + inline int src_idx(int x, int y) const { + return (src_y + y) * src_w + (src_x + x); + } +}; + +// ============================================================================ +// Saturation Policies - type-specific clamping behavior +// ============================================================================ + +struct FloatPolicy { + using Type = float; + + static float clamp(float v) { return v; } // No clamping for float + static float clamp(int v) { return static_cast(v); } + static float from_int(int v) { return static_cast(v); } + static float from_float(float v) { return v; } + static float zero() { return 0.0f; } + static float one() { return 1.0f; } +}; + +struct Uint8Policy { + using Type = uint8_t; + + static uint8_t clamp(int v) { + return static_cast(std::clamp(v, 0, 255)); + } + static uint8_t clamp(float v) { + return static_cast(std::clamp(static_cast(v), 0, 255)); + } + static uint8_t from_int(int v) { return clamp(v); } + static uint8_t from_float(float v) { return clamp(static_cast(v)); } + static uint8_t zero() { return 0; } + static uint8_t one() { return 1; } +}; + +// ============================================================================ +// Region Parameter Parsing - shared helpers +// ============================================================================ + +namespace MapOpsInternal { + +// Parse optional position tuple, returning (0, 0) if None/not provided +inline bool parseOptionalPos(PyObject* pos_obj, int* out_x, int* out_y, const char* param_name) { + *out_x = 0; + *out_y = 0; + + if (!pos_obj || pos_obj == Py_None) { + return true; // Default to (0, 0) + } + + // Try to parse as tuple/list of 2 ints + if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { + PyObject* x_obj = PyTuple_GetItem(pos_obj, 0); + PyObject* y_obj = PyTuple_GetItem(pos_obj, 1); + if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { + *out_x = (int)PyLong_AsLong(x_obj); + *out_y = (int)PyLong_AsLong(y_obj); + return true; + } + } else if (PyList_Check(pos_obj) && PyList_Size(pos_obj) == 2) { + PyObject* x_obj = PyList_GetItem(pos_obj, 0); + PyObject* y_obj = PyList_GetItem(pos_obj, 1); + if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { + *out_x = (int)PyLong_AsLong(x_obj); + *out_y = (int)PyLong_AsLong(y_obj); + return true; + } + } + + PyErr_Format(PyExc_TypeError, "%s must be a (x, y) tuple or list", param_name); + return false; +} + +// Parse optional size tuple +inline bool parseOptionalSize(PyObject* size_obj, int* out_w, int* out_h, const char* param_name) { + *out_w = -1; // -1 means "not specified" + *out_h = -1; + + if (!size_obj || size_obj == Py_None) { + return true; + } + + if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { + PyObject* w_obj = PyTuple_GetItem(size_obj, 0); + PyObject* h_obj = PyTuple_GetItem(size_obj, 1); + if (PyLong_Check(w_obj) && PyLong_Check(h_obj)) { + *out_w = (int)PyLong_AsLong(w_obj); + *out_h = (int)PyLong_AsLong(h_obj); + if (*out_w <= 0 || *out_h <= 0) { + PyErr_Format(PyExc_ValueError, "%s dimensions must be positive", param_name); + return false; + } + return true; + } + } else if (PyList_Check(size_obj) && PyList_Size(size_obj) == 2) { + PyObject* w_obj = PyList_GetItem(size_obj, 0); + PyObject* h_obj = PyList_GetItem(size_obj, 1); + if (PyLong_Check(w_obj) && PyLong_Check(h_obj)) { + *out_w = (int)PyLong_AsLong(w_obj); + *out_h = (int)PyLong_AsLong(h_obj); + if (*out_w <= 0 || *out_h <= 0) { + PyErr_Format(PyExc_ValueError, "%s dimensions must be positive", param_name); + return false; + } + return true; + } + } + + PyErr_Format(PyExc_TypeError, "%s must be a (width, height) tuple or list", param_name); + return false; +} + +} // namespace MapOpsInternal + +// ============================================================================ +// parseMapRegion - Parse region parameters for binary operations +// ============================================================================ + +// For binary operations (two maps) +inline bool parseMapRegion( + int dest_w, int dest_h, + int src_w, int src_h, + PyObject* pos, // (x, y) or None - destination position + PyObject* source_pos, // (x, y) or None - source position + PyObject* size, // (w, h) or None + MapRegion& out +) { + using namespace MapOpsInternal; + + // Store full dimensions + out.dest_w = dest_w; + out.dest_h = dest_h; + out.src_w = src_w; + out.src_h = src_h; + + // Parse positions, default to (0, 0) + if (!parseOptionalPos(pos, &out.dest_x, &out.dest_y, "pos")) { + return false; + } + if (!parseOptionalPos(source_pos, &out.src_x, &out.src_y, "source_pos")) { + return false; + } + + // Validate positions are within bounds + if (out.dest_x < 0 || out.dest_y < 0) { + PyErr_SetString(PyExc_ValueError, "pos coordinates cannot be negative"); + return false; + } + if (out.dest_x >= out.dest_w || out.dest_y >= out.dest_h) { + PyErr_Format(PyExc_ValueError, + "pos (%d, %d) is out of bounds for destination of size (%d, %d)", + out.dest_x, out.dest_y, out.dest_w, out.dest_h); + return false; + } + if (out.src_x < 0 || out.src_y < 0) { + PyErr_SetString(PyExc_ValueError, "source_pos coordinates cannot be negative"); + return false; + } + if (out.src_x >= out.src_w || out.src_y >= out.src_h) { + PyErr_Format(PyExc_ValueError, + "source_pos (%d, %d) is out of bounds for source of size (%d, %d)", + out.src_x, out.src_y, out.src_w, out.src_h); + return false; + } + + // Calculate remaining space from each position + int dest_remaining_w = out.dest_w - out.dest_x; + int dest_remaining_h = out.dest_h - out.dest_y; + int src_remaining_w = out.src_w - out.src_x; + int src_remaining_h = out.src_h - out.src_y; + + // Parse or infer size + int requested_w = -1, requested_h = -1; + if (!parseOptionalSize(size, &requested_w, &requested_h, "size")) { + return false; + } + + if (requested_w > 0 && requested_h > 0) { + // Explicit size - must fit in both + if (requested_w > dest_remaining_w || requested_h > dest_remaining_h) { + PyErr_Format(PyExc_ValueError, + "size (%d, %d) exceeds available space in destination (%d, %d) from pos (%d, %d)", + requested_w, requested_h, dest_remaining_w, dest_remaining_h, + out.dest_x, out.dest_y); + return false; + } + if (requested_w > src_remaining_w || requested_h > src_remaining_h) { + PyErr_Format(PyExc_ValueError, + "size (%d, %d) exceeds available space in source (%d, %d) from source_pos (%d, %d)", + requested_w, requested_h, src_remaining_w, src_remaining_h, + out.src_x, out.src_y); + return false; + } + out.width = requested_w; + out.height = requested_h; + } else { + // Infer size: smaller of remaining space in each + out.width = std::min(dest_remaining_w, src_remaining_w); + out.height = std::min(dest_remaining_h, src_remaining_h); + } + + // Final validation: non-zero region + if (out.width <= 0 || out.height <= 0) { + PyErr_SetString(PyExc_ValueError, "computed region has zero size"); + return false; + } + + return true; +} + +// For scalar operations (single map, just destination region) +inline bool parseMapRegionScalar( + int dest_w, int dest_h, + PyObject* pos, + PyObject* size, + MapRegion& out +) { + return parseMapRegion(dest_w, dest_h, dest_w, dest_h, pos, nullptr, size, out); +} + +// ============================================================================ +// Core map operations as free functions (used by both HeightMap and DiscreteMap) +// ============================================================================ + +namespace MapOps { + +// Fill region with value +template +void fill(typename Policy::Type* data, int w, int h, + typename Policy::Type value, const MapRegion& region) { + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + data[region.dest_idx(x, y)] = value; + } + } +} + +// Clear (fill with zero) +template +void clear(typename Policy::Type* data, int w, int h, const MapRegion& region) { + fill(data, w, h, Policy::zero(), region); +} + +// Copy from source (same type) +template +void copy(typename Policy::Type* dst, const typename Policy::Type* src, + const MapRegion& region) { + using T = typename Policy::Type; + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + dst[region.dest_idx(x, y)] = src[region.src_idx(x, y)]; + } + } +} + +// Add with saturation +template +void add(typename Policy::Type* dst, const typename Policy::Type* src, + const MapRegion& region) { + using T = typename Policy::Type; + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + // Use int accumulator to detect overflow + int result = static_cast(dst[idx]) + static_cast(src[region.src_idx(x, y)]); + dst[idx] = Policy::clamp(result); + } + } +} + +// Add scalar +template +void add_scalar(typename Policy::Type* data, int w, int h, + typename Policy::Type value, const MapRegion& region) { + using T = typename Policy::Type; + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + int result = static_cast(data[idx]) + static_cast(value); + data[idx] = Policy::clamp(result); + } + } +} + +// Subtract with saturation +template +void subtract(typename Policy::Type* dst, const typename Policy::Type* src, + const MapRegion& region) { + using T = typename Policy::Type; + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + int result = static_cast(dst[idx]) - static_cast(src[region.src_idx(x, y)]); + dst[idx] = Policy::clamp(result); + } + } +} + +// Multiply by scalar +template +void multiply_scalar(typename Policy::Type* data, int w, int h, + float factor, const MapRegion& region) { + using T = typename Policy::Type; + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + float result = static_cast(data[idx]) * factor; + data[idx] = Policy::clamp(result); + } + } +} + +// Element-wise max +template +void element_max(typename Policy::Type* dst, const typename Policy::Type* src, + const MapRegion& region) { + using T = typename Policy::Type; + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + T src_val = src[region.src_idx(x, y)]; + if (src_val > dst[idx]) dst[idx] = src_val; + } + } +} + +// Element-wise min +template +void element_min(typename Policy::Type* dst, const typename Policy::Type* src, + const MapRegion& region) { + using T = typename Policy::Type; + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + T src_val = src[region.src_idx(x, y)]; + if (src_val < dst[idx]) dst[idx] = src_val; + } + } +} + +} // namespace MapOps + +// ============================================================================ +// Cross-type operations (HeightMap <-> DiscreteMap conversion) +// ============================================================================ + +namespace MapConvert { + +// Copy float to uint8_t (floors and clamps) +inline void float_to_uint8(uint8_t* dst, const float* src, const MapRegion& region) { + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + float val = src[region.src_idx(x, y)]; + dst[region.dest_idx(x, y)] = Uint8Policy::clamp(val); + } + } +} + +// Copy uint8_t to float (simple promotion) +inline void uint8_to_float(float* dst, const uint8_t* src, const MapRegion& region) { + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + dst[region.dest_idx(x, y)] = static_cast(src[region.src_idx(x, y)]); + } + } +} + +// Add float to uint8_t (with clamping) +inline void add_float_to_uint8(uint8_t* dst, const float* src, const MapRegion& region) { + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + float result = static_cast(dst[idx]) + src[region.src_idx(x, y)]; + dst[idx] = Uint8Policy::clamp(result); + } + } +} + +// Add uint8_t to float +inline void add_uint8_to_float(float* dst, const uint8_t* src, const MapRegion& region) { + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + dst[idx] += static_cast(src[region.src_idx(x, y)]); + } + } +} + +} // namespace MapConvert + +// ============================================================================ +// Uint8-only bitwise operations +// ============================================================================ + +namespace MapBitwise { + +inline void bitwise_and(uint8_t* dst, const uint8_t* src, const MapRegion& region) { + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + dst[region.dest_idx(x, y)] &= src[region.src_idx(x, y)]; + } + } +} + +inline void bitwise_or(uint8_t* dst, const uint8_t* src, const MapRegion& region) { + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + dst[region.dest_idx(x, y)] |= src[region.src_idx(x, y)]; + } + } +} + +inline void bitwise_xor(uint8_t* dst, const uint8_t* src, const MapRegion& region) { + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + dst[region.dest_idx(x, y)] ^= src[region.src_idx(x, y)]; + } + } +} + +inline void invert(uint8_t* data, int w, int h, const MapRegion& region) { + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + data[idx] = 255 - data[idx]; + } + } +} + +} // namespace MapBitwise diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index a5096b3..d60c7a7 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -23,6 +23,7 @@ #include "PyMouse.h" #include "UIGridPathfinding.h" // AStarPath and DijkstraMap types #include "PyHeightMap.h" // Procedural generation heightmap (#193) +#include "PyDiscreteMap.h" // Procedural generation discrete map (#193) #include "PyBSP.h" // Procedural generation BSP (#202-206) #include "PyNoiseSource.h" // Procedural generation noise (#207-208) #include "PyLock.h" // Thread synchronization (#219) @@ -464,6 +465,7 @@ PyObject* PyInit_mcrfpy() /*procedural generation (#192)*/ &mcrfpydef::PyHeightMapType, + &mcrfpydef::PyDiscreteMapType, &mcrfpydef::PyBSPType, &mcrfpydef::PyNoiseSourceType, @@ -510,6 +512,10 @@ PyObject* PyInit_mcrfpy() mcrfpydef::PyHeightMapType.tp_methods = PyHeightMap::methods; mcrfpydef::PyHeightMapType.tp_getset = PyHeightMap::getsetters; + // Set up PyDiscreteMapType methods and getsetters (#193) + mcrfpydef::PyDiscreteMapType.tp_methods = PyDiscreteMap::methods; + mcrfpydef::PyDiscreteMapType.tp_getset = PyDiscreteMap::getsetters; + // Set up PyBSPType and BSPNode methods and getsetters (#202-206) mcrfpydef::PyBSPType.tp_methods = PyBSP::methods; mcrfpydef::PyBSPType.tp_getset = PyBSP::getsetters; diff --git a/src/PyDiscreteMap.cpp b/src/PyDiscreteMap.cpp new file mode 100644 index 0000000..ccb600b --- /dev/null +++ b/src/PyDiscreteMap.cpp @@ -0,0 +1,1548 @@ +#include "PyDiscreteMap.h" +#include "McRFPy_API.h" +#include "McRFPy_Doc.h" +#include "PyPositionHelper.h" +#include "PyHeightMap.h" +#include "MapOps.h" +#include +#include // for memset +#include + +// ============================================================================ +// Helper: Parse an integer value from PyObject (handles int and IntEnum) +// ============================================================================ +static bool parseIntValue(PyObject* value_obj, int* out_val) { + if (PyLong_Check(value_obj)) { + *out_val = (int)PyLong_AsLong(value_obj); + return true; + } + + // Try IntEnum (has .value attribute) + PyObject* val_attr = PyObject_GetAttrString(value_obj, "value"); + if (val_attr) { + if (PyLong_Check(val_attr)) { + *out_val = (int)PyLong_AsLong(val_attr); + Py_DECREF(val_attr); + return true; + } + Py_DECREF(val_attr); + } + PyErr_Clear(); + + PyErr_SetString(PyExc_TypeError, "value must be an integer or IntEnum member"); + return false; +} + +// ============================================================================ +// Helper: Convert uint8_t value to Python object (int or enum member) +// ============================================================================ +static PyObject* valueToResult(uint8_t value, PyObject* enum_type) { + if (enum_type && enum_type != Py_None) { + // Try to get enum member by value + PyObject* val_obj = PyLong_FromLong(value); + PyObject* member = PyObject_Call(enum_type, PyTuple_Pack(1, val_obj), nullptr); + Py_DECREF(val_obj); + + if (member) { + return member; // Return enum member + } + // If no matching enum member, fall through to return int + PyErr_Clear(); + } + return PyLong_FromLong(value); +} + +// ============================================================================ +// Helper: Create a new DiscreteMap object with given dimensions +// ============================================================================ +static PyDiscreteMapObject* CreateNewDiscreteMap(int width, int height) { + // Get the DiscreteMap type from the module + PyObject* dmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "DiscreteMap"); + if (!dmap_type) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap type not found in module"); + return nullptr; + } + + // Create size tuple + PyObject* size_tuple = Py_BuildValue("(ii)", width, height); + if (!size_tuple) { + Py_DECREF(dmap_type); + return nullptr; + } + + // Create args tuple containing the size tuple + PyObject* args = PyTuple_Pack(1, size_tuple); + Py_DECREF(size_tuple); + if (!args) { + Py_DECREF(dmap_type); + return nullptr; + } + + // Create the new object + PyDiscreteMapObject* new_dmap = (PyDiscreteMapObject*)PyObject_Call(dmap_type, args, nullptr); + Py_DECREF(args); + Py_DECREF(dmap_type); + + if (!new_dmap) { + return nullptr; // Python error already set + } + + return new_dmap; +} + +// ============================================================================ +// Helper: Validate another DiscreteMap for binary operations +// ============================================================================ +static PyDiscreteMapObject* validateOtherDiscreteMapType(PyObject* other_obj, const char* method_name) { + // Get the DiscreteMap type from the module + PyObject* dmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "DiscreteMap"); + if (!dmap_type) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap type not found in module"); + return nullptr; + } + + // Check if other is a DiscreteMap + int is_dmap = PyObject_IsInstance(other_obj, dmap_type); + Py_DECREF(dmap_type); + + if (is_dmap < 0) { + return nullptr; // Error during check + } + + if (!is_dmap) { + PyErr_Format(PyExc_TypeError, "%s() requires a DiscreteMap argument", method_name); + return nullptr; + } + + PyDiscreteMapObject* other = (PyDiscreteMapObject*)other_obj; + + if (!other->values) { + PyErr_SetString(PyExc_RuntimeError, "Other DiscreteMap not initialized"); + return nullptr; + } + + return other; +} + +// ============================================================================ +// Property definitions +// ============================================================================ +PyGetSetDef PyDiscreteMap::getsetters[] = { + {"size", (getter)PyDiscreteMap::get_size, NULL, + MCRF_PROPERTY(size, "Dimensions (width, height) of the map. Read-only."), NULL}, + {"enum_type", (getter)PyDiscreteMap::get_enum_type, (setter)PyDiscreteMap::set_enum_type, + MCRF_PROPERTY(enum_type, "Optional IntEnum class for value interpretation."), NULL}, + {NULL} +}; + +// ============================================================================ +// Mapping methods for subscript support (dmap[x, y]) +// ============================================================================ +PyMappingMethods PyDiscreteMap::mapping_methods = { + .mp_length = nullptr, + .mp_subscript = (binaryfunc)PyDiscreteMap::subscript, + .mp_ass_subscript = (objobjargproc)PyDiscreteMap::subscript_assign +}; + +// ============================================================================ +// Method definitions +// ============================================================================ +PyMethodDef PyDiscreteMap::methods[] = { + {"fill", (PyCFunction)PyDiscreteMap::fill, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, fill, + MCRF_SIG("(value: int, *, pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Set cells in region to the specified value."), + MCRF_ARGS_START + MCRF_ARG("value", "The value to set (0-255, or IntEnum member)") + MCRF_ARG("pos", "Region start (x, y) in destination (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) to fill (default: remaining space)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"clear", (PyCFunction)PyDiscreteMap::clear, METH_NOARGS, + MCRF_METHOD(DiscreteMap, clear, + MCRF_SIG("()", "DiscreteMap"), + MCRF_DESC("Set all cells to 0. Equivalent to fill(0)."), + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"get", (PyCFunction)PyDiscreteMap::get, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, get, + MCRF_SIG("(x, y) or (pos)", "int | Enum"), + MCRF_DESC("Get the value at integer coordinates."), + MCRF_ARGS_START + MCRF_ARG("x, y", "Position as two ints, tuple, list, or Vector") + MCRF_RETURNS("int or enum member if enum_type is set") + MCRF_RAISES("IndexError", "Position is out of bounds") + )}, + {"set", (PyCFunction)PyDiscreteMap::set, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, set, + MCRF_SIG("(x: int, y: int, value: int)", "None"), + MCRF_DESC("Set the value at integer coordinates."), + MCRF_ARGS_START + MCRF_ARG("x", "X coordinate") + MCRF_ARG("y", "Y coordinate") + MCRF_ARG("value", "Value to set (0-255, or IntEnum member)") + MCRF_RAISES("IndexError", "Position is out of bounds") + MCRF_RAISES("ValueError", "Value out of range 0-255") + )}, + // Combination operations + {"add", (PyCFunction)PyDiscreteMap::add, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, add, + MCRF_SIG("(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Add values from another map or a scalar, with saturation to 0-255."), + MCRF_ARGS_START + MCRF_ARG("other", "DiscreteMap to add, or int scalar to add to all cells") + MCRF_ARG("pos", "Destination start (x, y) in self (default: (0, 0))") + MCRF_ARG("source_pos", "Source start (x, y) in other (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) (default: max overlapping area)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"subtract", (PyCFunction)PyDiscreteMap::subtract, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, subtract, + MCRF_SIG("(other: DiscreteMap | int, *, pos=None, source_pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Subtract values from another map or a scalar, with saturation to 0-255."), + MCRF_ARGS_START + MCRF_ARG("other", "DiscreteMap to subtract, or int scalar") + MCRF_ARG("pos", "Destination start (x, y) in self (default: (0, 0))") + MCRF_ARG("source_pos", "Source start (x, y) in other (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) (default: max overlapping area)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"multiply", (PyCFunction)PyDiscreteMap::multiply, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, multiply, + MCRF_SIG("(factor: float, *, pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Multiply values by a scalar factor, with saturation to 0-255."), + MCRF_ARGS_START + MCRF_ARG("factor", "Multiplier for each cell") + MCRF_ARG("pos", "Region start (x, y) (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) (default: entire map)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"copy_from", (PyCFunction)PyDiscreteMap::copy_from, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, copy_from, + MCRF_SIG("(other: DiscreteMap, *, pos=None, source_pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Copy values from another DiscreteMap into the specified region."), + MCRF_ARGS_START + MCRF_ARG("other", "DiscreteMap to copy from") + MCRF_ARG("pos", "Destination start (x, y) in self (default: (0, 0))") + MCRF_ARG("source_pos", "Source start (x, y) in other (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) (default: max overlapping area)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"max", (PyCFunction)PyDiscreteMap::dmap_max, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, max, + MCRF_SIG("(other: DiscreteMap, *, pos=None, source_pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Set each cell to the maximum of this and another DiscreteMap."), + MCRF_ARGS_START + MCRF_ARG("other", "DiscreteMap to compare with") + MCRF_ARG("pos", "Destination start (x, y) in self (default: (0, 0))") + MCRF_ARG("source_pos", "Source start (x, y) in other (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) (default: max overlapping area)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"min", (PyCFunction)PyDiscreteMap::dmap_min, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, min, + MCRF_SIG("(other: DiscreteMap, *, pos=None, source_pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Set each cell to the minimum of this and another DiscreteMap."), + MCRF_ARGS_START + MCRF_ARG("other", "DiscreteMap to compare with") + MCRF_ARG("pos", "Destination start (x, y) in self (default: (0, 0))") + MCRF_ARG("source_pos", "Source start (x, y) in other (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) (default: max overlapping area)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + // Bitwise operations + {"bitwise_and", (PyCFunction)PyDiscreteMap::bitwise_and, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, bitwise_and, + MCRF_SIG("(other: DiscreteMap, *, pos=None, source_pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Bitwise AND with another DiscreteMap."), + MCRF_ARGS_START + MCRF_ARG("other", "DiscreteMap for AND operation") + MCRF_ARG("pos", "Destination start (x, y) in self (default: (0, 0))") + MCRF_ARG("source_pos", "Source start (x, y) in other (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) (default: max overlapping area)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"bitwise_or", (PyCFunction)PyDiscreteMap::bitwise_or, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, bitwise_or, + MCRF_SIG("(other: DiscreteMap, *, pos=None, source_pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Bitwise OR with another DiscreteMap."), + MCRF_ARGS_START + MCRF_ARG("other", "DiscreteMap for OR operation") + MCRF_ARG("pos", "Destination start (x, y) in self (default: (0, 0))") + MCRF_ARG("source_pos", "Source start (x, y) in other (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) (default: max overlapping area)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"bitwise_xor", (PyCFunction)PyDiscreteMap::bitwise_xor, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, bitwise_xor, + MCRF_SIG("(other: DiscreteMap, *, pos=None, source_pos=None, size=None)", "DiscreteMap"), + MCRF_DESC("Bitwise XOR with another DiscreteMap."), + MCRF_ARGS_START + MCRF_ARG("other", "DiscreteMap for XOR operation") + MCRF_ARG("pos", "Destination start (x, y) in self (default: (0, 0))") + MCRF_ARG("source_pos", "Source start (x, y) in other (default: (0, 0))") + MCRF_ARG("size", "Region (width, height) (default: max overlapping area)") + MCRF_RETURNS("DiscreteMap: self, for method chaining") + )}, + {"invert", (PyCFunction)PyDiscreteMap::invert, METH_NOARGS, + MCRF_METHOD(DiscreteMap, invert, + MCRF_SIG("()", "DiscreteMap"), + MCRF_DESC("Return NEW DiscreteMap with (255 - value) for each cell."), + MCRF_RETURNS("DiscreteMap: new inverted map (original unchanged)") + )}, + // Query methods + {"count", (PyCFunction)PyDiscreteMap::count, METH_VARARGS, + MCRF_METHOD(DiscreteMap, count, + MCRF_SIG("(value: int)", "int"), + MCRF_DESC("Count cells with the specified value."), + MCRF_ARGS_START + MCRF_ARG("value", "Value to count (0-255)") + MCRF_RETURNS("int: Number of cells with that value") + )}, + {"count_range", (PyCFunction)PyDiscreteMap::count_range, METH_VARARGS, + MCRF_METHOD(DiscreteMap, count_range, + MCRF_SIG("(min_val: int, max_val: int)", "int"), + MCRF_DESC("Count cells with values in the specified range (inclusive)."), + MCRF_ARGS_START + MCRF_ARG("min_val", "Minimum value (inclusive)") + MCRF_ARG("max_val", "Maximum value (inclusive)") + MCRF_RETURNS("int: Number of cells in range") + )}, + {"min_max", (PyCFunction)PyDiscreteMap::min_max, METH_NOARGS, + MCRF_METHOD(DiscreteMap, min_max, + MCRF_SIG("()", "tuple[int, int]"), + MCRF_DESC("Get the minimum and maximum values in the map."), + MCRF_RETURNS("tuple[int, int]: (min_value, max_value)") + )}, + {"histogram", (PyCFunction)PyDiscreteMap::histogram, METH_NOARGS, + MCRF_METHOD(DiscreteMap, histogram, + MCRF_SIG("()", "dict[int, int]"), + MCRF_DESC("Get a histogram of value counts."), + MCRF_RETURNS("dict: {value: count} for all values present in the map") + )}, + // Boolean/mask operations + {"bool", (PyCFunction)PyDiscreteMap::to_bool, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, bool, + MCRF_SIG("(condition: int | set | callable)", "DiscreteMap"), + MCRF_DESC("Create binary mask from condition. Returns NEW DiscreteMap."), + MCRF_ARGS_START + MCRF_ARG("condition", "int: match that value; set: match any in set; callable: predicate") + MCRF_RETURNS("DiscreteMap: new map with 1 where condition true, 0 elsewhere") + )}, + {"mask", (PyCFunction)PyDiscreteMap::mask, METH_NOARGS, + MCRF_METHOD(DiscreteMap, mask, + MCRF_SIG("()", "memoryview"), + MCRF_DESC("Get raw uint8_t data as memoryview for libtcod compatibility."), + MCRF_RETURNS("memoryview: Direct access to internal buffer (read/write)") + )}, + // HeightMap integration + {"from_heightmap", (PyCFunction)PyDiscreteMap::from_heightmap, METH_VARARGS | METH_KEYWORDS | METH_CLASS, + MCRF_METHOD(DiscreteMap, from_heightmap, + MCRF_SIG("(hmap: HeightMap, mapping: list[tuple[tuple[float,float], int]], *, enum=None)", "DiscreteMap"), + MCRF_DESC("Create DiscreteMap from HeightMap using range-to-value mapping."), + MCRF_ARGS_START + MCRF_ARG("hmap", "HeightMap to convert") + MCRF_ARG("mapping", "List of ((min, max), value) tuples") + MCRF_ARG("enum", "Optional IntEnum class for value interpretation") + MCRF_RETURNS("DiscreteMap: new map with mapped values") + )}, + {"to_heightmap", (PyCFunction)PyDiscreteMap::to_heightmap, METH_VARARGS | METH_KEYWORDS, + MCRF_METHOD(DiscreteMap, to_heightmap, + MCRF_SIG("(mapping: dict[int, float] = None)", "HeightMap"), + MCRF_DESC("Convert to HeightMap, optionally mapping values to floats."), + MCRF_ARGS_START + MCRF_ARG("mapping", "Optional {int: float} mapping (default: direct cast)") + MCRF_RETURNS("HeightMap: new heightmap with converted values") + )}, + {NULL} +}; + +// ============================================================================ +// Constructor / Destructor +// ============================================================================ + +PyObject* PyDiscreteMap::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) +{ + PyDiscreteMapObject* self = (PyDiscreteMapObject*)type->tp_alloc(type, 0); + if (self) { + self->values = nullptr; + self->w = 0; + self->h = 0; + self->enum_type = nullptr; + } + return (PyObject*)self; +} + +int PyDiscreteMap::init(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* keywords[] = {"size", "fill", "enum", nullptr}; + PyObject* size_obj = nullptr; + int fill_value = 0; + PyObject* enum_obj = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|iO", const_cast(keywords), + &size_obj, &fill_value, &enum_obj)) { + 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; + } + + if (width > GRID_MAX || height > GRID_MAX) { + PyErr_Format(PyExc_ValueError, + "DiscreteMap dimensions cannot exceed %d (got %dx%d)", + GRID_MAX, width, height); + return -1; + } + + // Validate fill value + if (fill_value < 0 || fill_value > 255) { + PyErr_SetString(PyExc_ValueError, "fill value must be in range 0-255"); + return -1; + } + + // Clean up any existing data + if (self->values) { + delete[] self->values; + self->values = nullptr; + } + Py_XDECREF(self->enum_type); + self->enum_type = nullptr; + + // Allocate new array + size_t total_size = static_cast(width) * static_cast(height); + self->values = new (std::nothrow) uint8_t[total_size]; + if (!self->values) { + PyErr_SetString(PyExc_MemoryError, "Failed to allocate DiscreteMap"); + return -1; + } + + self->w = width; + self->h = height; + + // Fill with initial value + memset(self->values, static_cast(fill_value), total_size); + + // Store enum type if provided + if (enum_obj && enum_obj != Py_None) { + Py_INCREF(enum_obj); + self->enum_type = enum_obj; + } + + return 0; +} + +void PyDiscreteMap::dealloc(PyDiscreteMapObject* self) +{ + if (self->values) { + delete[] self->values; + self->values = nullptr; + } + Py_XDECREF(self->enum_type); + self->enum_type = nullptr; + Py_TYPE(self)->tp_free((PyObject*)self); +} + +PyObject* PyDiscreteMap::repr(PyObject* obj) +{ + PyDiscreteMapObject* self = (PyDiscreteMapObject*)obj; + std::ostringstream ss; + + if (self->values) { + ss << "w << " x " << self->h << ")"; + if (self->enum_type && self->enum_type != Py_None) { + PyObject* name = PyObject_GetAttrString(self->enum_type, "__name__"); + if (name) { + ss << " enum=" << PyUnicode_AsUTF8(name); + Py_DECREF(name); + } else { + PyErr_Clear(); + } + } + ss << ">"; + } else { + ss << ""; + } + + return PyUnicode_FromString(ss.str().c_str()); +} + +// ============================================================================ +// Properties +// ============================================================================ + +PyObject* PyDiscreteMap::get_size(PyDiscreteMapObject* self, void* closure) +{ + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + return Py_BuildValue("(ii)", self->w, self->h); +} + +PyObject* PyDiscreteMap::get_enum_type(PyDiscreteMapObject* self, void* closure) +{ + if (self->enum_type) { + Py_INCREF(self->enum_type); + return self->enum_type; + } + Py_RETURN_NONE; +} + +int PyDiscreteMap::set_enum_type(PyDiscreteMapObject* self, PyObject* value, void* closure) +{ + Py_XDECREF(self->enum_type); + if (value && value != Py_None) { + Py_INCREF(value); + self->enum_type = value; + } else { + self->enum_type = nullptr; + } + return 0; +} + +// ============================================================================ +// Basic Operations +// ============================================================================ + +PyObject* PyDiscreteMap::fill(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"value", "pos", "size", nullptr}; + PyObject* value_obj; + PyObject* pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", const_cast(kwlist), + &value_obj, &pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + int value; + if (!parseIntValue(value_obj, &value)) { + return nullptr; + } + + if (value < 0 || value > 255) { + PyErr_SetString(PyExc_ValueError, "value must be in range 0-255"); + return nullptr; + } + + // Parse region parameters + MapRegion region; + if (!parseMapRegionScalar(self->w, self->h, pos, size, region)) { + return nullptr; + } + + // Fill the region + MapOps::fill(self->values, self->w, self->h, + static_cast(value), region); + + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* PyDiscreteMap::clear(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)) +{ + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + memset(self->values, 0, static_cast(self->w) * static_cast(self->h)); + + Py_INCREF(self); + return (PyObject*)self; +} + +// ============================================================================ +// Cell Access +// ============================================================================ + +PyObject* PyDiscreteMap::get(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + int x, y; + if (!PyPosition_ParseInt(args, kwds, &x, &y)) { + return nullptr; + } + + // Bounds check + if (x < 0 || x >= self->w || y < 0 || y >= self->h) { + PyErr_Format(PyExc_IndexError, + "Position (%d, %d) out of bounds for DiscreteMap of size (%d, %d)", + x, y, self->w, self->h); + return nullptr; + } + + uint8_t value = self->values[y * self->w + x]; + return valueToResult(value, self->enum_type); +} + +PyObject* PyDiscreteMap::set(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"x", "y", "value", nullptr}; + int x, y; + PyObject* value_obj; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiO", const_cast(kwlist), + &x, &y, &value_obj)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + // Bounds check + if (x < 0 || x >= self->w || y < 0 || y >= self->h) { + PyErr_Format(PyExc_IndexError, + "Position (%d, %d) out of bounds for DiscreteMap of size (%d, %d)", + x, y, self->w, self->h); + return nullptr; + } + + int value; + if (!parseIntValue(value_obj, &value)) { + return nullptr; + } + + if (value < 0 || value > 255) { + PyErr_SetString(PyExc_ValueError, "value must be in range 0-255"); + return nullptr; + } + + self->values[y * self->w + x] = static_cast(value); + + Py_RETURN_NONE; +} + +PyObject* PyDiscreteMap::subscript(PyDiscreteMapObject* self, PyObject* key) +{ + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + int x, y; + if (!PyPosition_FromObjectInt(key, &x, &y)) { + return nullptr; + } + + // Bounds check + if (x < 0 || x >= self->w || y < 0 || y >= self->h) { + PyErr_Format(PyExc_IndexError, + "Position (%d, %d) out of bounds for DiscreteMap of size (%d, %d)", + x, y, self->w, self->h); + return nullptr; + } + + uint8_t value = self->values[y * self->w + x]; + return valueToResult(value, self->enum_type); +} + +int PyDiscreteMap::subscript_assign(PyDiscreteMapObject* self, PyObject* key, PyObject* value) +{ + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return -1; + } + + // Handle deletion (not supported) + if (value == nullptr) { + PyErr_SetString(PyExc_TypeError, "cannot delete DiscreteMap elements"); + return -1; + } + + int x, y; + if (!PyPosition_FromObjectInt(key, &x, &y)) { + return -1; + } + + // Bounds check + if (x < 0 || x >= self->w || y < 0 || y >= self->h) { + PyErr_Format(PyExc_IndexError, + "Position (%d, %d) out of bounds for DiscreteMap of size (%d, %d)", + x, y, self->w, self->h); + return -1; + } + + int ival; + if (!parseIntValue(value, &ival)) { + return -1; + } + + if (ival < 0 || ival > 255) { + PyErr_SetString(PyExc_ValueError, "value must be in range 0-255"); + return -1; + } + + self->values[y * self->w + x] = static_cast(ival); + return 0; +} + +// ============================================================================ +// Combination Operations +// ============================================================================ + +PyObject* PyDiscreteMap::add(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"other", "pos", "source_pos", "size", nullptr}; + PyObject* other_obj; + PyObject* pos = nullptr; + PyObject* source_pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast(kwlist), + &other_obj, &pos, &source_pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + // Check if scalar or DiscreteMap + int scalar_val; + if (parseIntValue(other_obj, &scalar_val)) { + // Scalar add + MapRegion region; + if (!parseMapRegionScalar(self->w, self->h, pos, size, region)) { + return nullptr; + } + MapOps::add_scalar(self->values, self->w, self->h, + Uint8Policy::clamp(scalar_val), region); + } else { + PyErr_Clear(); // Clear the parseIntValue error + + PyDiscreteMapObject* other = validateOtherDiscreteMapType(other_obj, "add"); + if (!other) return nullptr; + + MapRegion region; + if (!parseMapRegion(self->w, self->h, other->w, other->h, + pos, source_pos, size, region)) { + return nullptr; + } + MapOps::add(self->values, other->values, region); + } + + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* PyDiscreteMap::subtract(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"other", "pos", "source_pos", "size", nullptr}; + PyObject* other_obj; + PyObject* pos = nullptr; + PyObject* source_pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast(kwlist), + &other_obj, &pos, &source_pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + // Check if scalar or DiscreteMap + int scalar_val; + if (parseIntValue(other_obj, &scalar_val)) { + // Scalar subtract (add negative) + MapRegion region; + if (!parseMapRegionScalar(self->w, self->h, pos, size, region)) { + return nullptr; + } + // Subtract by adding negative (with saturation) + for (int y = 0; y < region.height; y++) { + for (int x = 0; x < region.width; x++) { + int idx = region.dest_idx(x, y); + int result = static_cast(self->values[idx]) - scalar_val; + self->values[idx] = Uint8Policy::clamp(result); + } + } + } else { + PyErr_Clear(); + + PyDiscreteMapObject* other = validateOtherDiscreteMapType(other_obj, "subtract"); + if (!other) return nullptr; + + MapRegion region; + if (!parseMapRegion(self->w, self->h, other->w, other->h, + pos, source_pos, size, region)) { + return nullptr; + } + MapOps::subtract(self->values, other->values, region); + } + + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* PyDiscreteMap::multiply(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"factor", "pos", "size", nullptr}; + float factor; + PyObject* pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "f|OO", const_cast(kwlist), + &factor, &pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + MapRegion region; + if (!parseMapRegionScalar(self->w, self->h, pos, size, region)) { + return nullptr; + } + + MapOps::multiply_scalar(self->values, self->w, self->h, factor, region); + + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* PyDiscreteMap::copy_from(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"other", "pos", "source_pos", "size", nullptr}; + PyObject* other_obj; + PyObject* pos = nullptr; + PyObject* source_pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast(kwlist), + &other_obj, &pos, &source_pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + PyDiscreteMapObject* other = validateOtherDiscreteMapType(other_obj, "copy_from"); + if (!other) return nullptr; + + MapRegion region; + if (!parseMapRegion(self->w, self->h, other->w, other->h, + pos, source_pos, size, region)) { + return nullptr; + } + + MapOps::copy(self->values, other->values, region); + + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* PyDiscreteMap::dmap_max(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"other", "pos", "source_pos", "size", nullptr}; + PyObject* other_obj; + PyObject* pos = nullptr; + PyObject* source_pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast(kwlist), + &other_obj, &pos, &source_pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + PyDiscreteMapObject* other = validateOtherDiscreteMapType(other_obj, "max"); + if (!other) return nullptr; + + MapRegion region; + if (!parseMapRegion(self->w, self->h, other->w, other->h, + pos, source_pos, size, region)) { + return nullptr; + } + + MapOps::element_max(self->values, other->values, region); + + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* PyDiscreteMap::dmap_min(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"other", "pos", "source_pos", "size", nullptr}; + PyObject* other_obj; + PyObject* pos = nullptr; + PyObject* source_pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast(kwlist), + &other_obj, &pos, &source_pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + PyDiscreteMapObject* other = validateOtherDiscreteMapType(other_obj, "min"); + if (!other) return nullptr; + + MapRegion region; + if (!parseMapRegion(self->w, self->h, other->w, other->h, + pos, source_pos, size, region)) { + return nullptr; + } + + MapOps::element_min(self->values, other->values, region); + + Py_INCREF(self); + return (PyObject*)self; +} + +// ============================================================================ +// Bitwise Operations +// ============================================================================ + +PyObject* PyDiscreteMap::bitwise_and(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"other", "pos", "source_pos", "size", nullptr}; + PyObject* other_obj; + PyObject* pos = nullptr; + PyObject* source_pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast(kwlist), + &other_obj, &pos, &source_pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + PyDiscreteMapObject* other = validateOtherDiscreteMapType(other_obj, "bitwise_and"); + if (!other) return nullptr; + + MapRegion region; + if (!parseMapRegion(self->w, self->h, other->w, other->h, + pos, source_pos, size, region)) { + return nullptr; + } + + MapBitwise::bitwise_and(self->values, other->values, region); + + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* PyDiscreteMap::bitwise_or(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"other", "pos", "source_pos", "size", nullptr}; + PyObject* other_obj; + PyObject* pos = nullptr; + PyObject* source_pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast(kwlist), + &other_obj, &pos, &source_pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + PyDiscreteMapObject* other = validateOtherDiscreteMapType(other_obj, "bitwise_or"); + if (!other) return nullptr; + + MapRegion region; + if (!parseMapRegion(self->w, self->h, other->w, other->h, + pos, source_pos, size, region)) { + return nullptr; + } + + MapBitwise::bitwise_or(self->values, other->values, region); + + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* PyDiscreteMap::bitwise_xor(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"other", "pos", "source_pos", "size", nullptr}; + PyObject* other_obj; + PyObject* pos = nullptr; + PyObject* source_pos = nullptr; + PyObject* size = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast(kwlist), + &other_obj, &pos, &source_pos, &size)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + PyDiscreteMapObject* other = validateOtherDiscreteMapType(other_obj, "bitwise_xor"); + if (!other) return nullptr; + + MapRegion region; + if (!parseMapRegion(self->w, self->h, other->w, other->h, + pos, source_pos, size, region)) { + return nullptr; + } + + MapBitwise::bitwise_xor(self->values, other->values, region); + + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* PyDiscreteMap::invert(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)) +{ + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + // Create new DiscreteMap with same dimensions + PyDiscreteMapObject* result = CreateNewDiscreteMap(self->w, self->h); + if (!result) { + return nullptr; + } + + // Copy enum type + if (self->enum_type) { + Py_INCREF(self->enum_type); + result->enum_type = self->enum_type; + } + + // Set (255 - value) for each cell + size_t total = static_cast(self->w) * static_cast(self->h); + for (size_t i = 0; i < total; i++) { + result->values[i] = 255 - self->values[i]; + } + + return (PyObject*)result; +} + +// ============================================================================ +// Query Methods +// ============================================================================ + +PyObject* PyDiscreteMap::count(PyDiscreteMapObject* self, PyObject* args) +{ + PyObject* value_obj; + if (!PyArg_ParseTuple(args, "O", &value_obj)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + int value; + if (!parseIntValue(value_obj, &value)) { + return nullptr; + } + + if (value < 0 || value > 255) { + PyErr_SetString(PyExc_ValueError, "value must be in range 0-255"); + return nullptr; + } + + uint8_t target = static_cast(value); + long count = 0; + size_t total = static_cast(self->w) * static_cast(self->h); + for (size_t i = 0; i < total; i++) { + if (self->values[i] == target) count++; + } + + return PyLong_FromLong(count); +} + +PyObject* PyDiscreteMap::count_range(PyDiscreteMapObject* self, PyObject* args) +{ + int min_val, max_val; + if (!PyArg_ParseTuple(args, "ii", &min_val, &max_val)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + if (min_val > max_val) { + PyErr_SetString(PyExc_ValueError, "min must be <= max"); + return nullptr; + } + + // Clamp to valid range + min_val = std::max(0, min_val); + max_val = std::min(255, max_val); + + long count = 0; + size_t total = static_cast(self->w) * static_cast(self->h); + for (size_t i = 0; i < total; i++) { + int v = self->values[i]; + if (v >= min_val && v <= max_val) count++; + } + + return PyLong_FromLong(count); +} + +PyObject* PyDiscreteMap::min_max(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)) +{ + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + size_t total = static_cast(self->w) * static_cast(self->h); + if (total == 0) { + return Py_BuildValue("(ii)", 0, 0); + } + + uint8_t min_val = self->values[0]; + uint8_t max_val = self->values[0]; + + for (size_t i = 1; i < total; i++) { + if (self->values[i] < min_val) min_val = self->values[i]; + if (self->values[i] > max_val) max_val = self->values[i]; + } + + return Py_BuildValue("(ii)", min_val, max_val); +} + +PyObject* PyDiscreteMap::histogram(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)) +{ + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + // Count occurrences of each value + long counts[256] = {0}; + size_t total = static_cast(self->w) * static_cast(self->h); + for (size_t i = 0; i < total; i++) { + counts[self->values[i]]++; + } + + // Build dict with only non-zero counts + PyObject* result = PyDict_New(); + if (!result) return nullptr; + + for (int v = 0; v < 256; v++) { + if (counts[v] > 0) { + PyObject* key = PyLong_FromLong(v); + PyObject* val = PyLong_FromLong(counts[v]); + if (!key || !val || PyDict_SetItem(result, key, val) < 0) { + Py_XDECREF(key); + Py_XDECREF(val); + Py_DECREF(result); + return nullptr; + } + Py_DECREF(key); + Py_DECREF(val); + } + } + + return result; +} + +// ============================================================================ +// Boolean/Mask Operations +// ============================================================================ + +PyObject* PyDiscreteMap::to_bool(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"condition", nullptr}; + PyObject* condition; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", const_cast(kwlist), + &condition)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + // Create new DiscreteMap + PyDiscreteMapObject* result = CreateNewDiscreteMap(self->w, self->h); + if (!result) { + return nullptr; + } + + size_t total = static_cast(self->w) * static_cast(self->h); + + // Case 1: Integer - match exactly + int int_val; + if (parseIntValue(condition, &int_val)) { + if (int_val < 0 || int_val > 255) { + Py_DECREF(result); + PyErr_SetString(PyExc_ValueError, "condition value must be in range 0-255"); + return nullptr; + } + uint8_t target = static_cast(int_val); + for (size_t i = 0; i < total; i++) { + result->values[i] = (self->values[i] == target) ? 1 : 0; + } + return (PyObject*)result; + } + PyErr_Clear(); + + // Case 2: Set - match any value in set + if (PySet_Check(condition) || PyFrozenSet_Check(condition)) { + // Build quick lookup array + bool match[256] = {false}; + PyObject* iter = PyObject_GetIter(condition); + if (!iter) { + Py_DECREF(result); + return nullptr; + } + + PyObject* item; + while ((item = PyIter_Next(iter)) != nullptr) { + int v; + if (!parseIntValue(item, &v)) { + Py_DECREF(item); + Py_DECREF(iter); + Py_DECREF(result); + return nullptr; + } + Py_DECREF(item); + if (v >= 0 && v <= 255) { + match[v] = true; + } + } + Py_DECREF(iter); + + if (PyErr_Occurred()) { + Py_DECREF(result); + return nullptr; + } + + for (size_t i = 0; i < total; i++) { + result->values[i] = match[self->values[i]] ? 1 : 0; + } + return (PyObject*)result; + } + + // Case 3: Callable - predicate function + if (PyCallable_Check(condition)) { + for (size_t i = 0; i < total; i++) { + PyObject* arg = PyLong_FromLong(self->values[i]); + PyObject* res = PyObject_CallOneArg(condition, arg); + Py_DECREF(arg); + + if (!res) { + Py_DECREF(result); + return nullptr; + } + + int truth = PyObject_IsTrue(res); + Py_DECREF(res); + + if (truth < 0) { + Py_DECREF(result); + return nullptr; + } + + result->values[i] = truth ? 1 : 0; + } + return (PyObject*)result; + } + + Py_DECREF(result); + PyErr_SetString(PyExc_TypeError, + "condition must be an int, set of ints, or callable"); + return nullptr; +} + +PyObject* PyDiscreteMap::mask(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)) +{ + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + // Create memoryview of the internal buffer + Py_ssize_t len = static_cast(self->w) * static_cast(self->h); + return PyMemoryView_FromMemory(reinterpret_cast(self->values), len, PyBUF_WRITE); +} + +// ============================================================================ +// HeightMap Integration +// ============================================================================ + +PyObject* PyDiscreteMap::from_heightmap(PyTypeObject* type, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"hmap", "mapping", "enum", nullptr}; + PyObject* hmap_obj; + PyObject* mapping_obj; + PyObject* enum_obj = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|O", const_cast(kwlist), + &hmap_obj, &mapping_obj, &enum_obj)) { + return nullptr; + } + + // Validate HeightMap + PyObject* hmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap"); + if (!hmap_type) { + PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found in module"); + return nullptr; + } + + int is_hmap = PyObject_IsInstance(hmap_obj, hmap_type); + Py_DECREF(hmap_type); + + if (is_hmap < 0) { + return nullptr; + } + if (!is_hmap) { + PyErr_SetString(PyExc_TypeError, "First argument must be a HeightMap"); + return nullptr; + } + + PyHeightMapObject* hmap = (PyHeightMapObject*)hmap_obj; + if (!hmap->heightmap) { + PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized"); + return nullptr; + } + + // Parse mapping list: [((min, max), value), ...] + if (!PyList_Check(mapping_obj)) { + PyErr_SetString(PyExc_TypeError, "mapping must be a list of ((min, max), value) tuples"); + return nullptr; + } + + struct RangeMapping { + float min_val, max_val; + uint8_t target; + }; + std::vector mappings; + + Py_ssize_t n_mappings = PyList_Size(mapping_obj); + for (Py_ssize_t i = 0; i < n_mappings; i++) { + PyObject* item = PyList_GetItem(mapping_obj, i); + + if (!PyTuple_Check(item) || PyTuple_Size(item) != 2) { + PyErr_SetString(PyExc_TypeError, "each mapping must be a ((min, max), value) tuple"); + return nullptr; + } + + PyObject* range_obj = PyTuple_GetItem(item, 0); + PyObject* target_obj = PyTuple_GetItem(item, 1); + + if (!PyTuple_Check(range_obj) || PyTuple_Size(range_obj) != 2) { + PyErr_SetString(PyExc_TypeError, "range must be a (min, max) tuple"); + return nullptr; + } + + RangeMapping rm; + PyObject* min_obj = PyTuple_GetItem(range_obj, 0); + PyObject* max_obj = PyTuple_GetItem(range_obj, 1); + + if (PyFloat_Check(min_obj)) rm.min_val = (float)PyFloat_AsDouble(min_obj); + else if (PyLong_Check(min_obj)) rm.min_val = (float)PyLong_AsLong(min_obj); + else { + PyErr_SetString(PyExc_TypeError, "range values must be numeric"); + return nullptr; + } + + if (PyFloat_Check(max_obj)) rm.max_val = (float)PyFloat_AsDouble(max_obj); + else if (PyLong_Check(max_obj)) rm.max_val = (float)PyLong_AsLong(max_obj); + else { + PyErr_SetString(PyExc_TypeError, "range values must be numeric"); + return nullptr; + } + + int target_val; + if (!parseIntValue(target_obj, &target_val)) { + return nullptr; + } + if (target_val < 0 || target_val > 255) { + PyErr_SetString(PyExc_ValueError, "target value must be in range 0-255"); + return nullptr; + } + rm.target = static_cast(target_val); + + mappings.push_back(rm); + } + + // Create new DiscreteMap + int width = hmap->heightmap->w; + int height = hmap->heightmap->h; + + PyDiscreteMapObject* result = CreateNewDiscreteMap(width, height); + if (!result) { + return nullptr; + } + + // Store enum type if provided + if (enum_obj && enum_obj != Py_None) { + Py_INCREF(enum_obj); + result->enum_type = enum_obj; + } + + // Apply mappings + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + float val = hmap->heightmap->values[y * width + x]; + uint8_t mapped = 0; // Default if no mapping matches + + for (const auto& rm : mappings) { + if (val >= rm.min_val && val <= rm.max_val) { + mapped = rm.target; + break; // First match wins + } + } + + result->values[y * width + x] = mapped; + } + } + + return (PyObject*)result; +} + +PyObject* PyDiscreteMap::to_heightmap(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds) +{ + static const char* kwlist[] = {"mapping", nullptr}; + PyObject* mapping_obj = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", const_cast(kwlist), + &mapping_obj)) { + return nullptr; + } + + if (!self->values) { + PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized"); + return nullptr; + } + + // Parse optional mapping dict + float value_map[256]; + bool has_mapping = false; + + if (mapping_obj && mapping_obj != Py_None) { + if (!PyDict_Check(mapping_obj)) { + PyErr_SetString(PyExc_TypeError, "mapping must be a dict"); + return nullptr; + } + + // Initialize to direct cast as default + for (int i = 0; i < 256; i++) { + value_map[i] = static_cast(i); + } + + // Override with mapping values + PyObject* key; + PyObject* value; + Py_ssize_t pos = 0; + while (PyDict_Next(mapping_obj, &pos, &key, &value)) { + int k; + if (!parseIntValue(key, &k)) { + return nullptr; + } + if (k < 0 || k > 255) { + PyErr_SetString(PyExc_ValueError, "mapping keys must be in range 0-255"); + return nullptr; + } + + float v; + if (PyFloat_Check(value)) v = (float)PyFloat_AsDouble(value); + else if (PyLong_Check(value)) v = (float)PyLong_AsLong(value); + else { + PyErr_SetString(PyExc_TypeError, "mapping values must be numeric"); + return nullptr; + } + + value_map[k] = v; + } + has_mapping = true; + } + + // Get HeightMap type and create new instance + PyObject* hmap_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "HeightMap"); + if (!hmap_type) { + PyErr_SetString(PyExc_RuntimeError, "HeightMap type not found in module"); + return nullptr; + } + + PyObject* size_tuple = Py_BuildValue("(ii)", self->w, self->h); + if (!size_tuple) { + Py_DECREF(hmap_type); + return nullptr; + } + + PyObject* hmap_args = PyTuple_Pack(1, size_tuple); + Py_DECREF(size_tuple); + if (!hmap_args) { + Py_DECREF(hmap_type); + return nullptr; + } + + PyHeightMapObject* result = (PyHeightMapObject*)PyObject_Call(hmap_type, hmap_args, nullptr); + Py_DECREF(hmap_args); + Py_DECREF(hmap_type); + + if (!result) { + return nullptr; + } + + // Copy values with optional mapping + size_t total = static_cast(self->w) * static_cast(self->h); + for (size_t i = 0; i < total; i++) { + if (has_mapping) { + result->heightmap->values[i] = value_map[self->values[i]]; + } else { + result->heightmap->values[i] = static_cast(self->values[i]); + } + } + + return (PyObject*)result; +} diff --git a/src/PyDiscreteMap.h b/src/PyDiscreteMap.h new file mode 100644 index 0000000..c6b70b5 --- /dev/null +++ b/src/PyDiscreteMap.h @@ -0,0 +1,114 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include + +// Forward declaration +class PyDiscreteMap; + +// Python object structure +typedef struct { + PyObject_HEAD + uint8_t* values; // Row-major array (width * height) + int w, h; // Dimensions (max 8192x8192) + PyObject* enum_type; // Optional Python IntEnum for value interpretation +} PyDiscreteMapObject; + +class PyDiscreteMap +{ +public: + // Python type interface + static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds); + static int init(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static void dealloc(PyDiscreteMapObject* self); + static PyObject* repr(PyObject* obj); + + // Properties + static PyObject* get_size(PyDiscreteMapObject* self, void* closure); + static PyObject* get_enum_type(PyDiscreteMapObject* self, void* closure); + static int set_enum_type(PyDiscreteMapObject* self, PyObject* value, void* closure); + + // Scalar operations (all return self for chaining, support region parameters) + static PyObject* fill(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* clear(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)); + + // Cell access + static PyObject* get(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* set(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + + // Subscript support for dmap[x, y] syntax + static PyObject* subscript(PyDiscreteMapObject* self, PyObject* key); + static int subscript_assign(PyDiscreteMapObject* self, PyObject* key, PyObject* value); + + // Combination operations with region support + static PyObject* add(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* subtract(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* multiply(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* copy_from(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* dmap_max(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* dmap_min(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + + // Bitwise operations (DiscreteMap only) + static PyObject* bitwise_and(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* bitwise_or(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* bitwise_xor(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* invert(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)); + + // Query methods + static PyObject* count(PyDiscreteMapObject* self, PyObject* args); + static PyObject* count_range(PyDiscreteMapObject* self, PyObject* args); + static PyObject* min_max(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)); + static PyObject* histogram(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)); + + // Boolean/mask operations + static PyObject* to_bool(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + static PyObject* mask(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args)); + + // HeightMap integration + static PyObject* from_heightmap(PyTypeObject* type, PyObject* args, PyObject* kwds); + static PyObject* to_heightmap(PyDiscreteMapObject* self, PyObject* args, PyObject* kwds); + + // Mapping methods for subscript support + static PyMappingMethods mapping_methods; + + // Method and property definitions + static PyMethodDef methods[]; + static PyGetSetDef getsetters[]; +}; + +namespace mcrfpydef { + static PyTypeObject PyDiscreteMapType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.DiscreteMap", + .tp_basicsize = sizeof(PyDiscreteMapObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)PyDiscreteMap::dealloc, + .tp_repr = PyDiscreteMap::repr, + .tp_as_mapping = &PyDiscreteMap::mapping_methods, // dmap[x, y] subscript + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR( + "DiscreteMap(size: tuple[int, int], fill: int = 0, enum: type[IntEnum] = None)\n\n" + "A 2D grid of uint8 values (0-255) for discrete/categorical data.\n\n" + "DiscreteMap provides memory-efficient storage for terrain types, region IDs,\n" + "walkability masks, and other categorical data. Uses 4x less memory than HeightMap\n" + "for the same dimensions.\n\n" + "Args:\n" + " size: (width, height) dimensions. Immutable after creation.\n" + " fill: Initial value for all cells (0-255). Default 0.\n" + " enum: Optional IntEnum class for value interpretation.\n\n" + "Example:\n" + " from enum import IntEnum\n" + " class Terrain(IntEnum):\n" + " WATER = 0\n" + " GRASS = 1\n" + " MOUNTAIN = 2\n\n" + " dmap = mcrfpy.DiscreteMap((100, 100), fill=0, enum=Terrain)\n" + " dmap.fill(Terrain.GRASS, pos=(10, 10), size=(20, 20))\n" + " print(dmap[15, 15]) # Terrain.GRASS\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)PyDiscreteMap::init, + .tp_new = PyDiscreteMap::pynew, + }; +} diff --git a/tests/unit/discretemap_arithmetic_test.py b/tests/unit/discretemap_arithmetic_test.py new file mode 100644 index 0000000..80495fe --- /dev/null +++ b/tests/unit/discretemap_arithmetic_test.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""Unit tests for DiscreteMap arithmetic and bitwise operations.""" + +import mcrfpy +import sys + +def test_add_scalar(): + """Test adding a scalar value.""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=50) + dmap.add(25) + assert dmap[5, 5] == 75, f"Expected 75, got {dmap[5, 5]}" + + # Test saturation at 255 + dmap.fill(250) + dmap.add(20) # 250 + 20 = 270 -> saturates to 255 + assert dmap[0, 0] == 255, f"Expected 255 (saturated), got {dmap[0, 0]}" + + print(" [PASS] Add scalar") + +def test_add_map(): + """Test adding another DiscreteMap.""" + dmap1 = mcrfpy.DiscreteMap((10, 10), fill=50) + dmap2 = mcrfpy.DiscreteMap((10, 10), fill=30) + + dmap1.add(dmap2) + assert dmap1[5, 5] == 80, f"Expected 80, got {dmap1[5, 5]}" + + # Test saturation + dmap1.fill(200) + dmap2.fill(100) + dmap1.add(dmap2) + assert dmap1[0, 0] == 255, f"Expected 255 (saturated), got {dmap1[0, 0]}" + + print(" [PASS] Add map") + +def test_subtract_scalar(): + """Test subtracting a scalar value.""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=100) + dmap.subtract(25) + assert dmap[5, 5] == 75, f"Expected 75, got {dmap[5, 5]}" + + # Test saturation at 0 + dmap.fill(10) + dmap.subtract(20) # 10 - 20 = -10 -> saturates to 0 + assert dmap[0, 0] == 0, f"Expected 0 (saturated), got {dmap[0, 0]}" + + print(" [PASS] Subtract scalar") + +def test_subtract_map(): + """Test subtracting another DiscreteMap.""" + dmap1 = mcrfpy.DiscreteMap((10, 10), fill=100) + dmap2 = mcrfpy.DiscreteMap((10, 10), fill=30) + + dmap1.subtract(dmap2) + assert dmap1[5, 5] == 70, f"Expected 70, got {dmap1[5, 5]}" + + # Test saturation + dmap1.fill(50) + dmap2.fill(100) + dmap1.subtract(dmap2) + assert dmap1[0, 0] == 0, f"Expected 0 (saturated), got {dmap1[0, 0]}" + + print(" [PASS] Subtract map") + +def test_multiply(): + """Test scalar multiplication.""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=50) + dmap.multiply(2.0) + assert dmap[5, 5] == 100, f"Expected 100, got {dmap[5, 5]}" + + # Test saturation + dmap.fill(100) + dmap.multiply(3.0) # 100 * 3 = 300 -> saturates to 255 + assert dmap[0, 0] == 255, f"Expected 255 (saturated), got {dmap[0, 0]}" + + # Test fractional + dmap.fill(100) + dmap.multiply(0.5) + assert dmap[0, 0] == 50, f"Expected 50, got {dmap[0, 0]}" + + print(" [PASS] Multiply") + +def test_copy_from(): + """Test copy_from operation.""" + dmap1 = mcrfpy.DiscreteMap((10, 10), fill=0) + dmap2 = mcrfpy.DiscreteMap((5, 5), fill=99) + + dmap1.copy_from(dmap2, pos=(2, 2)) + + assert dmap1[2, 2] == 99, f"Expected 99 at (2,2), got {dmap1[2, 2]}" + assert dmap1[6, 6] == 99, f"Expected 99 at (6,6), got {dmap1[6, 6]}" + assert dmap1[0, 0] == 0, f"Expected 0 at (0,0), got {dmap1[0, 0]}" + assert dmap1[7, 7] == 0, f"Expected 0 at (7,7), got {dmap1[7, 7]}" + + print(" [PASS] Copy from") + +def test_max(): + """Test element-wise max.""" + dmap1 = mcrfpy.DiscreteMap((10, 10), fill=50) + dmap2 = mcrfpy.DiscreteMap((10, 10), fill=70) + + # Set some values in dmap1 higher + dmap1[3, 3] = 100 + + dmap1.max(dmap2) + + assert dmap1[0, 0] == 70, f"Expected 70 at (0,0), got {dmap1[0, 0]}" + assert dmap1[3, 3] == 100, f"Expected 100 at (3,3), got {dmap1[3, 3]}" + + print(" [PASS] Max") + +def test_min(): + """Test element-wise min.""" + dmap1 = mcrfpy.DiscreteMap((10, 10), fill=50) + dmap2 = mcrfpy.DiscreteMap((10, 10), fill=30) + + # Set some values in dmap1 lower + dmap1[3, 3] = 10 + + dmap1.min(dmap2) + + assert dmap1[0, 0] == 30, f"Expected 30 at (0,0), got {dmap1[0, 0]}" + assert dmap1[3, 3] == 10, f"Expected 10 at (3,3), got {dmap1[3, 3]}" + + print(" [PASS] Min") + +def test_bitwise_and(): + """Test bitwise AND.""" + dmap1 = mcrfpy.DiscreteMap((10, 10), fill=0xFF) # 11111111 + dmap2 = mcrfpy.DiscreteMap((10, 10), fill=0x0F) # 00001111 + + dmap1.bitwise_and(dmap2) + assert dmap1[0, 0] == 0x0F, f"Expected 0x0F, got {hex(dmap1[0, 0])}" + + # Test specific pattern + dmap1.fill(0b10101010) + dmap2.fill(0b11110000) + dmap1.bitwise_and(dmap2) + assert dmap1[0, 0] == 0b10100000, f"Expected 0b10100000, got {bin(dmap1[0, 0])}" + + print(" [PASS] Bitwise AND") + +def test_bitwise_or(): + """Test bitwise OR.""" + dmap1 = mcrfpy.DiscreteMap((10, 10), fill=0x0F) # 00001111 + dmap2 = mcrfpy.DiscreteMap((10, 10), fill=0xF0) # 11110000 + + dmap1.bitwise_or(dmap2) + assert dmap1[0, 0] == 0xFF, f"Expected 0xFF, got {hex(dmap1[0, 0])}" + + print(" [PASS] Bitwise OR") + +def test_bitwise_xor(): + """Test bitwise XOR.""" + dmap1 = mcrfpy.DiscreteMap((10, 10), fill=0xFF) + dmap2 = mcrfpy.DiscreteMap((10, 10), fill=0xFF) + + dmap1.bitwise_xor(dmap2) + assert dmap1[0, 0] == 0x00, f"Expected 0x00, got {hex(dmap1[0, 0])}" + + dmap1.fill(0b10101010) + dmap2.fill(0b11110000) + dmap1.bitwise_xor(dmap2) + assert dmap1[0, 0] == 0b01011010, f"Expected 0b01011010, got {bin(dmap1[0, 0])}" + + print(" [PASS] Bitwise XOR") + +def test_invert(): + """Test invert (returns new map).""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=100) + result = dmap.invert() + + # Original unchanged + assert dmap[0, 0] == 100, f"Original should be unchanged, got {dmap[0, 0]}" + + # Result is inverted + assert result[0, 0] == 155, f"Expected 155 (255-100), got {result[0, 0]}" + + # Test edge cases + dmap.fill(0) + result = dmap.invert() + assert result[0, 0] == 255, f"Expected 255, got {result[0, 0]}" + + dmap.fill(255) + result = dmap.invert() + assert result[0, 0] == 0, f"Expected 0, got {result[0, 0]}" + + print(" [PASS] Invert") + +def test_region_operations(): + """Test operations with region parameters.""" + dmap1 = mcrfpy.DiscreteMap((20, 20), fill=10) + dmap2 = mcrfpy.DiscreteMap((20, 20), fill=5) + + # Add only in a region + dmap1.add(dmap2, pos=(5, 5), source_pos=(0, 0), size=(5, 5)) + + assert dmap1[5, 5] == 15, f"Expected 15 in region, got {dmap1[5, 5]}" + assert dmap1[9, 9] == 15, f"Expected 15 in region, got {dmap1[9, 9]}" + assert dmap1[0, 0] == 10, f"Expected 10 outside region, got {dmap1[0, 0]}" + assert dmap1[10, 10] == 10, f"Expected 10 outside region, got {dmap1[10, 10]}" + + print(" [PASS] Region operations") + +def main(): + print("Running DiscreteMap arithmetic tests...") + + test_add_scalar() + test_add_map() + test_subtract_scalar() + test_subtract_map() + test_multiply() + test_copy_from() + test_max() + test_min() + test_bitwise_and() + test_bitwise_or() + test_bitwise_xor() + test_invert() + test_region_operations() + + print("All DiscreteMap arithmetic tests PASSED!") + sys.exit(0) + +if __name__ == "__main__": + main() diff --git a/tests/unit/discretemap_basic_test.py b/tests/unit/discretemap_basic_test.py new file mode 100644 index 0000000..9dbd4d2 --- /dev/null +++ b/tests/unit/discretemap_basic_test.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Unit tests for DiscreteMap basic operations.""" + +import mcrfpy +import sys + +def test_construction(): + """Test basic construction.""" + # Default construction + dmap = mcrfpy.DiscreteMap((100, 100)) + assert dmap.size == (100, 100), f"Expected (100, 100), got {dmap.size}" + + # With fill value + dmap2 = mcrfpy.DiscreteMap((50, 50), fill=42) + assert dmap2[0, 0] == 42, f"Expected 42, got {dmap2[0, 0]}" + assert dmap2[25, 25] == 42, f"Expected 42, got {dmap2[25, 25]}" + + print(" [PASS] Construction") + +def test_size_property(): + """Test size property.""" + dmap = mcrfpy.DiscreteMap((123, 456)) + w, h = dmap.size + assert w == 123, f"Expected width 123, got {w}" + assert h == 456, f"Expected height 456, got {h}" + print(" [PASS] Size property") + +def test_get_set(): + """Test get/set methods.""" + dmap = mcrfpy.DiscreteMap((10, 10)) + + # Test set/get + dmap.set(5, 5, 100) + assert dmap.get(5, 5) == 100, f"Expected 100, got {dmap.get(5, 5)}" + + # Test subscript + dmap[3, 7] = 200 + assert dmap[3, 7] == 200, f"Expected 200, got {dmap[3, 7]}" + + # Test tuple subscript + dmap[(1, 2)] = 150 + assert dmap[(1, 2)] == 150, f"Expected 150, got {dmap[(1, 2)]}" + + print(" [PASS] Get/set methods") + +def test_bounds_checking(): + """Test that out-of-bounds access raises IndexError.""" + dmap = mcrfpy.DiscreteMap((10, 10)) + + # Test out of bounds get + try: + _ = dmap[10, 10] + print(" [FAIL] Should have raised IndexError for (10, 10)") + return False + except IndexError: + pass + + try: + _ = dmap[-1, 0] + print(" [FAIL] Should have raised IndexError for (-1, 0)") + return False + except IndexError: + pass + + # Test out of bounds set + try: + dmap[100, 100] = 5 + print(" [FAIL] Should have raised IndexError for set") + return False + except IndexError: + pass + + print(" [PASS] Bounds checking") + return True + +def test_value_range(): + """Test that values are clamped to 0-255.""" + dmap = mcrfpy.DiscreteMap((10, 10)) + + # Test valid range + dmap[0, 0] = 0 + dmap[0, 1] = 255 + assert dmap[0, 0] == 0 + assert dmap[0, 1] == 255 + + # Test invalid values + try: + dmap[0, 0] = -1 + print(" [FAIL] Should have raised ValueError for -1") + return False + except ValueError: + pass + + try: + dmap[0, 0] = 256 + print(" [FAIL] Should have raised ValueError for 256") + return False + except ValueError: + pass + + print(" [PASS] Value range") + return True + +def test_fill(): + """Test fill operation.""" + dmap = mcrfpy.DiscreteMap((10, 10)) + + # Fill entire map + dmap.fill(77) + for y in range(10): + for x in range(10): + assert dmap[x, y] == 77, f"Expected 77 at ({x}, {y}), got {dmap[x, y]}" + + # Fill region + dmap.fill(88, pos=(2, 2), size=(3, 3)) + assert dmap[2, 2] == 88, "Region fill failed at start" + assert dmap[4, 4] == 88, "Region fill failed at end" + assert dmap[1, 1] == 77, "Region fill affected outside area" + assert dmap[5, 5] == 77, "Region fill affected outside area" + + print(" [PASS] Fill operation") + +def test_clear(): + """Test clear operation.""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=100) + dmap.clear() + + for y in range(10): + for x in range(10): + assert dmap[x, y] == 0, f"Expected 0 at ({x}, {y}), got {dmap[x, y]}" + + print(" [PASS] Clear operation") + +def test_repr(): + """Test repr output.""" + dmap = mcrfpy.DiscreteMap((100, 50)) + r = repr(dmap) + assert "DiscreteMap" in r, f"Expected 'DiscreteMap' in repr, got {r}" + assert "100" in r, f"Expected '100' in repr, got {r}" + assert "50" in r, f"Expected '50' in repr, got {r}" + print(" [PASS] Repr") + +def test_chaining(): + """Test method chaining.""" + dmap = mcrfpy.DiscreteMap((10, 10)) + + # Methods should return self + result = dmap.fill(50).clear().fill(100) + assert result is dmap, "Method chaining should return self" + assert dmap[5, 5] == 100, "Chained operations should work" + + print(" [PASS] Method chaining") + +def main(): + print("Running DiscreteMap basic tests...") + + test_construction() + test_size_property() + test_get_set() + if not test_bounds_checking(): + sys.exit(1) + if not test_value_range(): + sys.exit(1) + test_fill() + test_clear() + test_repr() + test_chaining() + + print("All DiscreteMap basic tests PASSED!") + sys.exit(0) + +if __name__ == "__main__": + main() diff --git a/tests/unit/discretemap_heightmap_test.py b/tests/unit/discretemap_heightmap_test.py new file mode 100644 index 0000000..d72b2fd --- /dev/null +++ b/tests/unit/discretemap_heightmap_test.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +"""Unit tests for DiscreteMap <-> HeightMap integration.""" + +import mcrfpy +import sys +from enum import IntEnum + +class Terrain(IntEnum): + WATER = 0 + SAND = 1 + GRASS = 2 + FOREST = 3 + MOUNTAIN = 4 + +def test_from_heightmap_basic(): + """Test basic HeightMap to DiscreteMap conversion.""" + # Create a simple heightmap + hmap = mcrfpy.HeightMap((10, 10), fill=0.5) + + # Create a simple mapping + mapping = [ + ((0.0, 0.3), 0), + ((0.3, 0.6), 1), + ((0.6, 1.0), 2), + ] + + dmap = mcrfpy.DiscreteMap.from_heightmap(hmap, mapping) + + # 0.5 should map to category 1 + assert dmap[5, 5] == 1, f"Expected 1, got {dmap[5, 5]}" + + print(" [PASS] from_heightmap basic") + +def test_from_heightmap_full_range(): + """Test conversion with values spanning the full range.""" + hmap = mcrfpy.HeightMap((100, 1)) + + # Create gradient + for x in range(100): + hmap[x, 0] = x / 100.0 # 0.0 to 0.99 + + mapping = [ + ((0.0, 0.25), Terrain.WATER), + ((0.25, 0.5), Terrain.SAND), + ((0.5, 0.75), Terrain.GRASS), + ((0.75, 1.0), Terrain.FOREST), + ] + + dmap = mcrfpy.DiscreteMap.from_heightmap(hmap, mapping) + + # Check values at key positions + assert dmap[10, 0] == Terrain.WATER, f"Expected WATER at 10, got {dmap[10, 0]}" + assert dmap[30, 0] == Terrain.SAND, f"Expected SAND at 30, got {dmap[30, 0]}" + assert dmap[60, 0] == Terrain.GRASS, f"Expected GRASS at 60, got {dmap[60, 0]}" + assert dmap[80, 0] == Terrain.FOREST, f"Expected FOREST at 80, got {dmap[80, 0]}" + + print(" [PASS] from_heightmap full range") + +def test_from_heightmap_with_enum(): + """Test from_heightmap with enum parameter.""" + hmap = mcrfpy.HeightMap((10, 10), fill=0.5) + + mapping = [ + ((0.0, 0.3), Terrain.WATER), + ((0.3, 0.7), Terrain.GRASS), + ((0.7, 1.0), Terrain.MOUNTAIN), + ] + + dmap = mcrfpy.DiscreteMap.from_heightmap(hmap, mapping, enum=Terrain) + + # Value should be returned as enum member + val = dmap[5, 5] + assert val == Terrain.GRASS, f"Expected Terrain.GRASS, got {val}" + assert isinstance(val, Terrain), f"Expected Terrain type, got {type(val)}" + + print(" [PASS] from_heightmap with enum") + +def test_to_heightmap_basic(): + """Test basic DiscreteMap to HeightMap conversion.""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=100) + + hmap = dmap.to_heightmap() + + # Direct conversion: uint8 -> float + assert abs(hmap[5, 5] - 100.0) < 0.001, f"Expected 100.0, got {hmap[5, 5]}" + + print(" [PASS] to_heightmap basic") + +def test_to_heightmap_with_mapping(): + """Test to_heightmap with value mapping.""" + dmap = mcrfpy.DiscreteMap((10, 10)) + + # Create pattern + dmap.fill(0, pos=(0, 0), size=(5, 10)) # Left half = 0 + dmap.fill(1, pos=(5, 0), size=(5, 10)) # Right half = 1 + + # Map discrete values to heights + mapping = { + 0: 0.2, + 1: 0.8, + } + + hmap = dmap.to_heightmap(mapping) + + assert abs(hmap[2, 5] - 0.2) < 0.001, f"Expected 0.2, got {hmap[2, 5]}" + assert abs(hmap[7, 5] - 0.8) < 0.001, f"Expected 0.8, got {hmap[7, 5]}" + + print(" [PASS] to_heightmap with mapping") + +def test_roundtrip(): + """Test HeightMap -> DiscreteMap -> HeightMap roundtrip.""" + # Create original heightmap + original = mcrfpy.HeightMap((50, 50)) + for y in range(50): + for x in range(50): + original[x, y] = (x + y) / 100.0 # Gradient 0.0 to 0.98 + + # Convert to discrete with specific ranges + mapping = [ + ((0.0, 0.33), 0), + ((0.33, 0.66), 1), + ((0.66, 1.0), 2), + ] + dmap = mcrfpy.DiscreteMap.from_heightmap(original, mapping) + + # Convert back with value mapping + reverse_mapping = { + 0: 0.15, # Midpoint of first range + 1: 0.5, # Midpoint of second range + 2: 0.85, # Midpoint of third range + } + restored = dmap.to_heightmap(reverse_mapping) + + # Verify approximate restoration + assert abs(restored[0, 0] - 0.15) < 0.01, f"Expected ~0.15 at (0,0), got {restored[0, 0]}" + assert abs(restored[25, 25] - 0.5) < 0.01, f"Expected ~0.5 at (25,25), got {restored[25, 25]}" + + print(" [PASS] Roundtrip conversion") + +def test_query_methods(): + """Test count, count_range, min_max, histogram.""" + dmap = mcrfpy.DiscreteMap((10, 10)) + + # Create pattern with different values + dmap.fill(0, pos=(0, 0), size=(5, 5)) # 25 cells with 0 + dmap.fill(1, pos=(5, 0), size=(5, 5)) # 25 cells with 1 + dmap.fill(2, pos=(0, 5), size=(5, 5)) # 25 cells with 2 + dmap.fill(3, pos=(5, 5), size=(5, 5)) # 25 cells with 3 + + # Test count + assert dmap.count(0) == 25, f"Expected 25 zeros, got {dmap.count(0)}" + assert dmap.count(1) == 25, f"Expected 25 ones, got {dmap.count(1)}" + assert dmap.count(4) == 0, f"Expected 0 fours, got {dmap.count(4)}" + + # Test count_range + assert dmap.count_range(0, 1) == 50, f"Expected 50 in range 0-1, got {dmap.count_range(0, 1)}" + assert dmap.count_range(0, 3) == 100, f"Expected 100 in range 0-3, got {dmap.count_range(0, 3)}" + + # Test min_max + min_val, max_val = dmap.min_max() + assert min_val == 0, f"Expected min 0, got {min_val}" + assert max_val == 3, f"Expected max 3, got {max_val}" + + # Test histogram + hist = dmap.histogram() + assert hist[0] == 25, f"Expected 25 for value 0, got {hist.get(0)}" + assert hist[1] == 25, f"Expected 25 for value 1, got {hist.get(1)}" + assert hist[2] == 25, f"Expected 25 for value 2, got {hist.get(2)}" + assert hist[3] == 25, f"Expected 25 for value 3, got {hist.get(3)}" + assert 4 not in hist, "Value 4 should not be in histogram" + + print(" [PASS] Query methods") + +def test_bool_int(): + """Test bool() with integer condition.""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=0) + dmap.fill(1, pos=(2, 2), size=(3, 3)) + + mask = dmap.bool(1) + + # Should be 1 where original is 1, 0 elsewhere + assert mask[0, 0] == 0, f"Expected 0 outside region, got {mask[0, 0]}" + assert mask[3, 3] == 1, f"Expected 1 inside region, got {mask[3, 3]}" + assert mask.count(1) == 9, f"Expected 9 ones, got {mask.count(1)}" + + print(" [PASS] bool() with int") + +def test_bool_set(): + """Test bool() with set condition.""" + dmap = mcrfpy.DiscreteMap((10, 10)) + dmap.fill(0, pos=(0, 0), size=(5, 5)) + dmap.fill(1, pos=(5, 0), size=(5, 5)) + dmap.fill(2, pos=(0, 5), size=(5, 5)) + dmap.fill(3, pos=(5, 5), size=(5, 5)) + + # Match 0 or 2 + mask = dmap.bool({0, 2}) + + assert mask[2, 2] == 1, "Expected 1 where value is 0" + assert mask[7, 2] == 0, "Expected 0 where value is 1" + assert mask[2, 7] == 1, "Expected 1 where value is 2" + assert mask[7, 7] == 0, "Expected 0 where value is 3" + assert mask.count(1) == 50, f"Expected 50 ones, got {mask.count(1)}" + + print(" [PASS] bool() with set") + +def test_bool_callable(): + """Test bool() with callable condition.""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=0) + for y in range(10): + for x in range(10): + dmap[x, y] = x + y # Values 0-18 + + # Match where value > 10 + mask = dmap.bool(lambda v: v > 10) + + assert mask[5, 5] == 0, "Expected 0 where value is 10" + assert mask[6, 6] == 1, "Expected 1 where value is 12" + assert mask[9, 9] == 1, "Expected 1 where value is 18" + + print(" [PASS] bool() with callable") + +def test_mask_memoryview(): + """Test mask() returns working memoryview.""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=42) + + mv = dmap.mask() + + assert len(mv) == 100, f"Expected 100 bytes, got {len(mv)}" + assert mv[0] == 42, f"Expected 42, got {mv[0]}" + + # Test writing through memoryview + mv[50] = 99 + assert dmap[0, 5] == 99, f"Expected 99, got {dmap[0, 5]}" + + print(" [PASS] mask() memoryview") + +def test_enum_type_property(): + """Test enum_type property getter/setter.""" + dmap = mcrfpy.DiscreteMap((10, 10), fill=1) + + # Initially no enum + assert dmap.enum_type is None, "Expected None initially" + + # Set enum type + dmap.enum_type = Terrain + assert dmap.enum_type is Terrain, "Expected Terrain enum" + + # Value should now return enum member + val = dmap[5, 5] + assert val == Terrain.SAND, f"Expected Terrain.SAND, got {val}" + + # Clear enum type + dmap.enum_type = None + val = dmap[5, 5] + assert isinstance(val, int), f"Expected int after clearing enum, got {type(val)}" + + print(" [PASS] enum_type property") + +def main(): + print("Running DiscreteMap HeightMap integration tests...") + + test_from_heightmap_basic() + test_from_heightmap_full_range() + test_from_heightmap_with_enum() + test_to_heightmap_basic() + test_to_heightmap_with_mapping() + test_roundtrip() + test_query_methods() + test_bool_int() + test_bool_set() + test_bool_callable() + test_mask_memoryview() + test_enum_type_property() + + print("All DiscreteMap HeightMap integration tests PASSED!") + sys.exit(0) + +if __name__ == "__main__": + main()