McRogueFace/src/PyDiscreteMap.cpp

1633 lines
56 KiB
C++
Raw Normal View History

#include "PyDiscreteMap.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include "PyPositionHelper.h"
#include "PyHeightMap.h"
#include "MapOps.h"
#include <sstream>
#include <cstring> // for memset
#include <algorithm>
// ============================================================================
// 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
PyObject* dmap_type = (PyObject*)&mcrfpydef::PyDiscreteMapType;
// Create size tuple
PyObject* size_tuple = Py_BuildValue("(ii)", width, height);
if (!size_tuple) {
return nullptr;
}
// Create args tuple containing the size tuple
PyObject* args = PyTuple_Pack(1, size_tuple);
Py_DECREF(size_tuple);
if (!args) {
return nullptr;
}
// Create the new object
PyDiscreteMapObject* new_dmap = (PyDiscreteMapObject*)PyObject_Call(dmap_type, args, nullptr);
Py_DECREF(args);
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) {
// Check if other is a DiscreteMap
int is_dmap = PyObject_IsInstance(other_obj, (PyObject*)&mcrfpydef::PyDiscreteMapType);
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)")
)},
// Serialization
{"to_bytes", (PyCFunction)PyDiscreteMap::to_bytes, METH_NOARGS,
MCRF_METHOD(DiscreteMap, to_bytes,
MCRF_SIG("()", "bytes"),
MCRF_DESC("Serialize map data to bytes (row-major, one byte per cell)."),
MCRF_RETURNS("bytes: Raw cell data, length = width * height")
)},
{"from_bytes", (PyCFunction)PyDiscreteMap::from_bytes, METH_VARARGS | METH_KEYWORDS | METH_CLASS,
MCRF_METHOD(DiscreteMap, from_bytes,
MCRF_SIG("(data: bytes, size: tuple[int, int], *, enum: type = None)", "DiscreteMap"),
MCRF_DESC("Create a DiscreteMap from raw byte data."),
MCRF_ARGS_START
MCRF_ARG("data", "Raw cell data (one byte per cell, row-major)")
MCRF_ARG("size", "(width, height) dimensions")
MCRF_ARG("enum", "Optional IntEnum class for value interpretation")
MCRF_RETURNS("DiscreteMap: new map initialized from data")
MCRF_RAISES("ValueError", "Data length does not match width * height")
)},
// 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) {
Replace UIEntity gridstate with DiscreteMap perspective_map; closes #294 Per-entity FOV memory moves from std::vector<UIGridPointState> (two-bool visible/discovered pairs) to a 3-state DiscreteMap (0=UNKNOWN, 1=DISCOVERED, 2=VISIBLE), exposed as entity.perspective_map. The invariant visible-subset-of-discovered becomes structural (single value per cell), and the map is a live, serializable, first-class object rather than an implicit internal array. Changes: - New DiscreteMap C++ class with shared ownership; PyDiscreteMapObject now holds shared_ptr<DiscreteMap>. UIEntity holds the same shared_ptr. - New mcrfpy.Perspective IntEnum (UNKNOWN/DISCOVERED/VISIBLE), modelled on PyInputState. - entity.perspective_map: lazy-allocated on first access with a grid; setter validates size against grid and raises ValueError on mismatch; None clears (next access lazy-reallocates fresh). - updateVisibility() now demotes 2->1 then promotes visible cells to 2. - entity.at(x, y) returns grid.at(x, y) when VISIBLE, else None. - Fog-of-war rendering in UIGridView and UIGrid reads the 3-state map. - Removed: UIEntity::gridstate, ensureGridstate(), entity.gridstate getter, UIGridPointState struct + PyUIGridPointStateType. - Obsolete tests deleted (test_gridpointstate_point, issue_265_gridpointstate_dangle); 4 new tests cover lazy allocation, identity, serialization round-trip, size validation, and the visible-subset-of-discovered invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:27 -04:00
// Placement-new the shared_ptr member; tp_alloc zeroed memory but
// shared_ptr requires proper construction before assignment.
new (&self->data) std::shared_ptr<DiscreteMap>();
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<char**>(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;
}
Replace UIEntity gridstate with DiscreteMap perspective_map; closes #294 Per-entity FOV memory moves from std::vector<UIGridPointState> (two-bool visible/discovered pairs) to a 3-state DiscreteMap (0=UNKNOWN, 1=DISCOVERED, 2=VISIBLE), exposed as entity.perspective_map. The invariant visible-subset-of-discovered becomes structural (single value per cell), and the map is a live, serializable, first-class object rather than an implicit internal array. Changes: - New DiscreteMap C++ class with shared ownership; PyDiscreteMapObject now holds shared_ptr<DiscreteMap>. UIEntity holds the same shared_ptr. - New mcrfpy.Perspective IntEnum (UNKNOWN/DISCOVERED/VISIBLE), modelled on PyInputState. - entity.perspective_map: lazy-allocated on first access with a grid; setter validates size against grid and raises ValueError on mismatch; None clears (next access lazy-reallocates fresh). - updateVisibility() now demotes 2->1 then promotes visible cells to 2. - entity.at(x, y) returns grid.at(x, y) when VISIBLE, else None. - Fog-of-war rendering in UIGridView and UIGrid reads the 3-state map. - Removed: UIEntity::gridstate, ensureGridstate(), entity.gridstate getter, UIGridPointState struct + PyUIGridPointStateType. - Obsolete tests deleted (test_gridpointstate_point, issue_265_gridpointstate_dangle); 4 new tests cover lazy allocation, identity, serialization round-trip, size validation, and the visible-subset-of-discovered invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:27 -04:00
// Reset any existing storage (re-init supported)
self->data.reset();
Py_XDECREF(self->enum_type);
self->enum_type = nullptr;
Replace UIEntity gridstate with DiscreteMap perspective_map; closes #294 Per-entity FOV memory moves from std::vector<UIGridPointState> (two-bool visible/discovered pairs) to a 3-state DiscreteMap (0=UNKNOWN, 1=DISCOVERED, 2=VISIBLE), exposed as entity.perspective_map. The invariant visible-subset-of-discovered becomes structural (single value per cell), and the map is a live, serializable, first-class object rather than an implicit internal array. Changes: - New DiscreteMap C++ class with shared ownership; PyDiscreteMapObject now holds shared_ptr<DiscreteMap>. UIEntity holds the same shared_ptr. - New mcrfpy.Perspective IntEnum (UNKNOWN/DISCOVERED/VISIBLE), modelled on PyInputState. - entity.perspective_map: lazy-allocated on first access with a grid; setter validates size against grid and raises ValueError on mismatch; None clears (next access lazy-reallocates fresh). - updateVisibility() now demotes 2->1 then promotes visible cells to 2. - entity.at(x, y) returns grid.at(x, y) when VISIBLE, else None. - Fog-of-war rendering in UIGridView and UIGrid reads the 3-state map. - Removed: UIEntity::gridstate, ensureGridstate(), entity.gridstate getter, UIGridPointState struct + PyUIGridPointStateType. - Obsolete tests deleted (test_gridpointstate_point, issue_265_gridpointstate_dangle); 4 new tests cover lazy allocation, identity, serialization round-trip, size validation, and the visible-subset-of-discovered invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:27 -04:00
// Construct shared-ownership C++ storage (issue #294)
try {
self->data = std::make_shared<DiscreteMap>(
width, height, static_cast<uint8_t>(fill_value));
} catch (const std::bad_alloc&) {
PyErr_SetString(PyExc_MemoryError, "Failed to allocate DiscreteMap");
return -1;
}
Replace UIEntity gridstate with DiscreteMap perspective_map; closes #294 Per-entity FOV memory moves from std::vector<UIGridPointState> (two-bool visible/discovered pairs) to a 3-state DiscreteMap (0=UNKNOWN, 1=DISCOVERED, 2=VISIBLE), exposed as entity.perspective_map. The invariant visible-subset-of-discovered becomes structural (single value per cell), and the map is a live, serializable, first-class object rather than an implicit internal array. Changes: - New DiscreteMap C++ class with shared ownership; PyDiscreteMapObject now holds shared_ptr<DiscreteMap>. UIEntity holds the same shared_ptr. - New mcrfpy.Perspective IntEnum (UNKNOWN/DISCOVERED/VISIBLE), modelled on PyInputState. - entity.perspective_map: lazy-allocated on first access with a grid; setter validates size against grid and raises ValueError on mismatch; None clears (next access lazy-reallocates fresh). - updateVisibility() now demotes 2->1 then promotes visible cells to 2. - entity.at(x, y) returns grid.at(x, y) when VISIBLE, else None. - Fog-of-war rendering in UIGridView and UIGrid reads the 3-state map. - Removed: UIEntity::gridstate, ensureGridstate(), entity.gridstate getter, UIGridPointState struct + PyUIGridPointStateType. - Obsolete tests deleted (test_gridpointstate_point, issue_265_gridpointstate_dangle); 4 new tests cover lazy allocation, identity, serialization round-trip, size validation, and the visible-subset-of-discovered invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:27 -04:00
// Cache non-owning views for hot-path access
self->values = self->data->data();
self->w = self->data->width();
self->h = self->data->height();
// 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)
{
Replace UIEntity gridstate with DiscreteMap perspective_map; closes #294 Per-entity FOV memory moves from std::vector<UIGridPointState> (two-bool visible/discovered pairs) to a 3-state DiscreteMap (0=UNKNOWN, 1=DISCOVERED, 2=VISIBLE), exposed as entity.perspective_map. The invariant visible-subset-of-discovered becomes structural (single value per cell), and the map is a live, serializable, first-class object rather than an implicit internal array. Changes: - New DiscreteMap C++ class with shared ownership; PyDiscreteMapObject now holds shared_ptr<DiscreteMap>. UIEntity holds the same shared_ptr. - New mcrfpy.Perspective IntEnum (UNKNOWN/DISCOVERED/VISIBLE), modelled on PyInputState. - entity.perspective_map: lazy-allocated on first access with a grid; setter validates size against grid and raises ValueError on mismatch; None clears (next access lazy-reallocates fresh). - updateVisibility() now demotes 2->1 then promotes visible cells to 2. - entity.at(x, y) returns grid.at(x, y) when VISIBLE, else None. - Fog-of-war rendering in UIGridView and UIGrid reads the 3-state map. - Removed: UIEntity::gridstate, ensureGridstate(), entity.gridstate getter, UIGridPointState struct + PyUIGridPointStateType. - Obsolete tests deleted (test_gridpointstate_point, issue_265_gridpointstate_dangle); 4 new tests cover lazy allocation, identity, serialization round-trip, size validation, and the visible-subset-of-discovered invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:27 -04:00
// Release shared ownership; DiscreteMap destructor frees the buffer
// if this is the last owner.
self->data.~shared_ptr<DiscreteMap>();
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 << "<DiscreteMap (" << self->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 << "<DiscreteMap (uninitialized)>";
}
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<char**>(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<Uint8Policy>(self->values, self->w, self->h,
static_cast<uint8_t>(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<size_t>(self->w) * static_cast<size_t>(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<char**>(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<uint8_t>(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<uint8_t>(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<char**>(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<Uint8Policy>(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<Uint8Policy>(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<char**>(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<int>(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<Uint8Policy>(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<char**>(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<Uint8Policy>(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<char**>(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<Uint8Policy>(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<char**>(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<Uint8Policy>(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<char**>(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<Uint8Policy>(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<char**>(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<char**>(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<char**>(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<size_t>(self->w) * static_cast<size_t>(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<uint8_t>(value);
long count = 0;
size_t total = static_cast<size_t>(self->w) * static_cast<size_t>(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<size_t>(self->w) * static_cast<size_t>(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<size_t>(self->w) * static_cast<size_t>(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<size_t>(self->w) * static_cast<size_t>(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<char**>(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<size_t>(self->w) * static_cast<size_t>(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<uint8_t>(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<Py_ssize_t>(self->w) * static_cast<Py_ssize_t>(self->h);
return PyMemoryView_FromMemory(reinterpret_cast<char*>(self->values), len, PyBUF_WRITE);
}
// ============================================================================
// Serialization
// ============================================================================
PyObject* PyDiscreteMap::to_bytes(PyDiscreteMapObject* self, PyObject* Py_UNUSED(args))
{
if (!self->values) {
PyErr_SetString(PyExc_RuntimeError, "DiscreteMap not initialized");
return nullptr;
}
Py_ssize_t len = static_cast<Py_ssize_t>(self->w) * static_cast<Py_ssize_t>(self->h);
return PyBytes_FromStringAndSize(reinterpret_cast<const char*>(self->values), len);
}
PyObject* PyDiscreteMap::from_bytes(PyTypeObject* type, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"data", "size", "enum", nullptr};
Py_buffer buffer;
PyObject* size_obj = nullptr;
PyObject* enum_type = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "y*O|O", const_cast<char**>(kwlist),
&buffer, &size_obj, &enum_type)) {
return nullptr;
}
int w = 0, h = 0;
if (!PyArg_ParseTuple(size_obj, "ii", &w, &h)) {
PyErr_Clear();
if (PyTuple_Check(size_obj) && PyTuple_GET_SIZE(size_obj) == 2) {
w = (int)PyLong_AsLong(PyTuple_GET_ITEM(size_obj, 0));
h = (int)PyLong_AsLong(PyTuple_GET_ITEM(size_obj, 1));
if (PyErr_Occurred()) {
PyBuffer_Release(&buffer);
return nullptr;
}
} else {
PyBuffer_Release(&buffer);
PyErr_SetString(PyExc_TypeError, "size must be a (width, height) tuple");
return nullptr;
}
}
if (w <= 0 || h <= 0 || w > 8192 || h > 8192) {
PyBuffer_Release(&buffer);
PyErr_SetString(PyExc_ValueError, "dimensions must be positive and <= 8192");
return nullptr;
}
Py_ssize_t expected = static_cast<Py_ssize_t>(w) * static_cast<Py_ssize_t>(h);
if (buffer.len != expected) {
PyBuffer_Release(&buffer);
PyErr_Format(PyExc_ValueError,
"data length (%zd) does not match size %d x %d = %zd",
buffer.len, w, h, expected);
return nullptr;
}
if (enum_type && enum_type != Py_None && !PyType_Check(enum_type)) {
PyBuffer_Release(&buffer);
PyErr_SetString(PyExc_TypeError, "enum must be a type (IntEnum subclass)");
return nullptr;
}
auto obj = (PyDiscreteMapObject*)type->tp_alloc(type, 0);
if (!obj) {
PyBuffer_Release(&buffer);
return nullptr;
}
Replace UIEntity gridstate with DiscreteMap perspective_map; closes #294 Per-entity FOV memory moves from std::vector<UIGridPointState> (two-bool visible/discovered pairs) to a 3-state DiscreteMap (0=UNKNOWN, 1=DISCOVERED, 2=VISIBLE), exposed as entity.perspective_map. The invariant visible-subset-of-discovered becomes structural (single value per cell), and the map is a live, serializable, first-class object rather than an implicit internal array. Changes: - New DiscreteMap C++ class with shared ownership; PyDiscreteMapObject now holds shared_ptr<DiscreteMap>. UIEntity holds the same shared_ptr. - New mcrfpy.Perspective IntEnum (UNKNOWN/DISCOVERED/VISIBLE), modelled on PyInputState. - entity.perspective_map: lazy-allocated on first access with a grid; setter validates size against grid and raises ValueError on mismatch; None clears (next access lazy-reallocates fresh). - updateVisibility() now demotes 2->1 then promotes visible cells to 2. - entity.at(x, y) returns grid.at(x, y) when VISIBLE, else None. - Fog-of-war rendering in UIGridView and UIGrid reads the 3-state map. - Removed: UIEntity::gridstate, ensureGridstate(), entity.gridstate getter, UIGridPointState struct + PyUIGridPointStateType. - Obsolete tests deleted (test_gridpointstate_point, issue_265_gridpointstate_dangle); 4 new tests cover lazy allocation, identity, serialization round-trip, size validation, and the visible-subset-of-discovered invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:27 -04:00
new (&obj->data) std::shared_ptr<DiscreteMap>();
try {
obj->data = std::make_shared<DiscreteMap>(w, h, 0);
} catch (const std::bad_alloc&) {
PyBuffer_Release(&buffer);
Replace UIEntity gridstate with DiscreteMap perspective_map; closes #294 Per-entity FOV memory moves from std::vector<UIGridPointState> (two-bool visible/discovered pairs) to a 3-state DiscreteMap (0=UNKNOWN, 1=DISCOVERED, 2=VISIBLE), exposed as entity.perspective_map. The invariant visible-subset-of-discovered becomes structural (single value per cell), and the map is a live, serializable, first-class object rather than an implicit internal array. Changes: - New DiscreteMap C++ class with shared ownership; PyDiscreteMapObject now holds shared_ptr<DiscreteMap>. UIEntity holds the same shared_ptr. - New mcrfpy.Perspective IntEnum (UNKNOWN/DISCOVERED/VISIBLE), modelled on PyInputState. - entity.perspective_map: lazy-allocated on first access with a grid; setter validates size against grid and raises ValueError on mismatch; None clears (next access lazy-reallocates fresh). - updateVisibility() now demotes 2->1 then promotes visible cells to 2. - entity.at(x, y) returns grid.at(x, y) when VISIBLE, else None. - Fog-of-war rendering in UIGridView and UIGrid reads the 3-state map. - Removed: UIEntity::gridstate, ensureGridstate(), entity.gridstate getter, UIGridPointState struct + PyUIGridPointStateType. - Obsolete tests deleted (test_gridpointstate_point, issue_265_gridpointstate_dangle); 4 new tests cover lazy allocation, identity, serialization round-trip, size validation, and the visible-subset-of-discovered invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:27 -04:00
obj->data.~shared_ptr<DiscreteMap>();
Py_DECREF(obj);
return PyErr_NoMemory();
}
Replace UIEntity gridstate with DiscreteMap perspective_map; closes #294 Per-entity FOV memory moves from std::vector<UIGridPointState> (two-bool visible/discovered pairs) to a 3-state DiscreteMap (0=UNKNOWN, 1=DISCOVERED, 2=VISIBLE), exposed as entity.perspective_map. The invariant visible-subset-of-discovered becomes structural (single value per cell), and the map is a live, serializable, first-class object rather than an implicit internal array. Changes: - New DiscreteMap C++ class with shared ownership; PyDiscreteMapObject now holds shared_ptr<DiscreteMap>. UIEntity holds the same shared_ptr. - New mcrfpy.Perspective IntEnum (UNKNOWN/DISCOVERED/VISIBLE), modelled on PyInputState. - entity.perspective_map: lazy-allocated on first access with a grid; setter validates size against grid and raises ValueError on mismatch; None clears (next access lazy-reallocates fresh). - updateVisibility() now demotes 2->1 then promotes visible cells to 2. - entity.at(x, y) returns grid.at(x, y) when VISIBLE, else None. - Fog-of-war rendering in UIGridView and UIGrid reads the 3-state map. - Removed: UIEntity::gridstate, ensureGridstate(), entity.gridstate getter, UIGridPointState struct + PyUIGridPointStateType. - Obsolete tests deleted (test_gridpointstate_point, issue_265_gridpointstate_dangle); 4 new tests cover lazy allocation, identity, serialization round-trip, size validation, and the visible-subset-of-discovered invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:04:27 -04:00
obj->w = w;
obj->h = h;
obj->values = obj->data->data();
std::memcpy(obj->values, buffer.buf, expected);
PyBuffer_Release(&buffer);
if (enum_type && enum_type != Py_None) {
Py_INCREF(enum_type);
obj->enum_type = enum_type;
} else {
obj->enum_type = nullptr;
}
return (PyObject*)obj;
}
// ============================================================================
// 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<char**>(kwlist),
&hmap_obj, &mapping_obj, &enum_obj)) {
return nullptr;
}
// Validate HeightMap
int is_hmap = PyObject_IsInstance(hmap_obj, (PyObject*)&mcrfpydef::PyHeightMapType);
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<RangeMapping> 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<uint8_t>(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<char**>(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<float>(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*)&mcrfpydef::PyHeightMapType;
PyObject* size_tuple = Py_BuildValue("(ii)", self->w, self->h);
if (!size_tuple) {
return nullptr;
}
PyObject* hmap_args = PyTuple_Pack(1, size_tuple);
Py_DECREF(size_tuple);
if (!hmap_args) {
return nullptr;
}
PyHeightMapObject* result = (PyHeightMapObject*)PyObject_Call(hmap_type, hmap_args, nullptr);
Py_DECREF(hmap_args);
if (!result) {
return nullptr;
}
// Copy values with optional mapping
size_t total = static_cast<size_t>(self->w) * static_cast<size_t>(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<float>(self->values[i]);
}
}
return (PyObject*)result;
}