DiscreteMap class - mask for operations or uint8 tile data
This commit is contained in:
parent
001cc6efd6
commit
d8fec5fea0
7 changed files with 2817 additions and 0 deletions
470
src/MapOps.h
Normal file
470
src/MapOps.h
Normal file
|
|
@ -0,0 +1,470 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Python.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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<float>(v); }
|
||||||
|
static float from_int(int v) { return static_cast<float>(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<uint8_t>(std::clamp(v, 0, 255));
|
||||||
|
}
|
||||||
|
static uint8_t clamp(float v) {
|
||||||
|
return static_cast<uint8_t>(std::clamp(static_cast<int>(v), 0, 255));
|
||||||
|
}
|
||||||
|
static uint8_t from_int(int v) { return clamp(v); }
|
||||||
|
static uint8_t from_float(float v) { return clamp(static_cast<int>(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<typename Policy>
|
||||||
|
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<typename Policy>
|
||||||
|
void clear(typename Policy::Type* data, int w, int h, const MapRegion& region) {
|
||||||
|
fill<Policy>(data, w, h, Policy::zero(), region);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy from source (same type)
|
||||||
|
template<typename Policy>
|
||||||
|
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<typename Policy>
|
||||||
|
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<int>(dst[idx]) + static_cast<int>(src[region.src_idx(x, y)]);
|
||||||
|
dst[idx] = Policy::clamp(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add scalar
|
||||||
|
template<typename Policy>
|
||||||
|
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<int>(data[idx]) + static_cast<int>(value);
|
||||||
|
data[idx] = Policy::clamp(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract with saturation
|
||||||
|
template<typename Policy>
|
||||||
|
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<int>(dst[idx]) - static_cast<int>(src[region.src_idx(x, y)]);
|
||||||
|
dst[idx] = Policy::clamp(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply by scalar
|
||||||
|
template<typename Policy>
|
||||||
|
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<float>(data[idx]) * factor;
|
||||||
|
data[idx] = Policy::clamp(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Element-wise max
|
||||||
|
template<typename Policy>
|
||||||
|
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<typename Policy>
|
||||||
|
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<float>(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<float>(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<float>(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
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
#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 "PyHeightMap.h" // Procedural generation heightmap (#193)
|
||||||
|
#include "PyDiscreteMap.h" // Procedural generation discrete map (#193)
|
||||||
#include "PyBSP.h" // Procedural generation BSP (#202-206)
|
#include "PyBSP.h" // Procedural generation BSP (#202-206)
|
||||||
#include "PyNoiseSource.h" // Procedural generation noise (#207-208)
|
#include "PyNoiseSource.h" // Procedural generation noise (#207-208)
|
||||||
#include "PyLock.h" // Thread synchronization (#219)
|
#include "PyLock.h" // Thread synchronization (#219)
|
||||||
|
|
@ -464,6 +465,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
|
|
||||||
/*procedural generation (#192)*/
|
/*procedural generation (#192)*/
|
||||||
&mcrfpydef::PyHeightMapType,
|
&mcrfpydef::PyHeightMapType,
|
||||||
|
&mcrfpydef::PyDiscreteMapType,
|
||||||
&mcrfpydef::PyBSPType,
|
&mcrfpydef::PyBSPType,
|
||||||
&mcrfpydef::PyNoiseSourceType,
|
&mcrfpydef::PyNoiseSourceType,
|
||||||
|
|
||||||
|
|
@ -510,6 +512,10 @@ PyObject* PyInit_mcrfpy()
|
||||||
mcrfpydef::PyHeightMapType.tp_methods = PyHeightMap::methods;
|
mcrfpydef::PyHeightMapType.tp_methods = PyHeightMap::methods;
|
||||||
mcrfpydef::PyHeightMapType.tp_getset = PyHeightMap::getsetters;
|
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)
|
// Set up PyBSPType and BSPNode methods and getsetters (#202-206)
|
||||||
mcrfpydef::PyBSPType.tp_methods = PyBSP::methods;
|
mcrfpydef::PyBSPType.tp_methods = PyBSP::methods;
|
||||||
mcrfpydef::PyBSPType.tp_getset = PyBSP::getsetters;
|
mcrfpydef::PyBSPType.tp_getset = PyBSP::getsetters;
|
||||||
|
|
|
||||||
1548
src/PyDiscreteMap.cpp
Normal file
1548
src/PyDiscreteMap.cpp
Normal file
File diff suppressed because it is too large
Load diff
114
src/PyDiscreteMap.h
Normal file
114
src/PyDiscreteMap.h
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Common.h"
|
||||||
|
#include "Python.h"
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
226
tests/unit/discretemap_arithmetic_test.py
Normal file
226
tests/unit/discretemap_arithmetic_test.py
Normal file
|
|
@ -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()
|
||||||
173
tests/unit/discretemap_basic_test.py
Normal file
173
tests/unit/discretemap_basic_test.py
Normal file
|
|
@ -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()
|
||||||
280
tests/unit/discretemap_heightmap_test.py
Normal file
280
tests/unit/discretemap_heightmap_test.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue