diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 2547819..b4beeed 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -120,9 +120,12 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) { auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); + Py_DECREF(type); obj->data = &(self->data->gridstate[y * self->data->grid->grid_x + x]); obj->grid = self->data->grid; obj->entity = self->data; + obj->x = x; // #16 - Store position for .point property + obj->y = y; return (PyObject*)obj; } @@ -312,23 +315,29 @@ sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) { } PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) { - // Create a new GridPointState Python object + // Create a new GridPointState Python object (detached - no grid/entity context) auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); if (!type) { return NULL; } - + auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); if (!obj) { Py_DECREF(type); return NULL; } - + // Allocate new data and copy values obj->data = new UIGridPointState(); obj->data->visible = state.visible; obj->data->discovered = state.discovered; - + + // Initialize context fields (detached state has no grid/entity context) + obj->grid = nullptr; + obj->entity = nullptr; + obj->x = -1; + obj->y = -1; + Py_DECREF(type); return (PyObject*)obj; } diff --git a/src/UIGridPoint.cpp b/src/UIGridPoint.cpp index df93fce..757e5c1 100644 --- a/src/UIGridPoint.cpp +++ b/src/UIGridPoint.cpp @@ -1,5 +1,6 @@ #include "UIGridPoint.h" #include "UIGrid.h" +#include "UIEntity.h" // #114 - for GridPoint.entities #include "GridLayers.h" // #150 - for GridLayerType, ColorLayer, TileLayer #include // #150 - for strcmp @@ -90,9 +91,52 @@ int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, voi // #150 - Removed get_int_member/set_int_member - now handled by layers +// #114 - Get list of entities at this grid cell +PyObject* UIGridPoint::get_entities(PyUIGridPointObject* self, void* closure) { + if (!self->grid) { + PyErr_SetString(PyExc_RuntimeError, "GridPoint has no parent grid"); + return NULL; + } + + int target_x = self->data->grid_x; + int target_y = self->data->grid_y; + + PyObject* list = PyList_New(0); + if (!list) return NULL; + + // Iterate through grid's entities and find those at this position + for (auto& entity : *(self->grid->entities)) { + if (static_cast(entity->position.x) == target_x && + static_cast(entity->position.y) == target_y) { + // Create Python Entity object for this entity + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); + if (!type) { + Py_DECREF(list); + return NULL; + } + auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0); + Py_DECREF(type); + if (!obj) { + Py_DECREF(list); + return NULL; + } + obj->data = entity; + if (PyList_Append(list, (PyObject*)obj) < 0) { + Py_DECREF(obj); + Py_DECREF(list); + return NULL; + } + Py_DECREF(obj); // List now owns the reference + } + } + + return list; +} + PyGetSetDef UIGridPoint::getsetters[] = { {"walkable", (getter)UIGridPoint::get_bool_member, (setter)UIGridPoint::set_bool_member, "Is the GridPoint walkable", (void*)0}, {"transparent", (getter)UIGridPoint::get_bool_member, (setter)UIGridPoint::set_bool_member, "Is the GridPoint transparent", (void*)1}, + {"entities", (getter)UIGridPoint::get_entities, NULL, "List of entities at this grid cell (read-only)", NULL}, {NULL} /* Sentinel */ }; @@ -137,9 +181,43 @@ int UIGridPointState::set_bool_member(PyUIGridPointStateObject* self, PyObject* return 0; } +// #16 - Get GridPoint at this position (None if not discovered) +PyObject* UIGridPointState::get_point(PyUIGridPointStateObject* self, void* closure) { + // Return None if entity hasn't discovered this cell + if (!self->data->discovered) { + Py_RETURN_NONE; + } + + if (!self->grid) { + PyErr_SetString(PyExc_RuntimeError, "GridPointState has no parent grid"); + return NULL; + } + + // Return the GridPoint at this position + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPoint"); + if (!type) return NULL; + + auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0); + Py_DECREF(type); + if (!obj) return NULL; + + // Get the GridPoint from the grid + int idx = self->y * self->grid->grid_x + self->x; + if (idx < 0 || idx >= static_cast(self->grid->points.size())) { + Py_DECREF(obj); + PyErr_SetString(PyExc_IndexError, "GridPointState position out of bounds"); + return NULL; + } + + obj->data = &(self->grid->points[idx]); + obj->grid = self->grid; + return (PyObject*)obj; +} + PyGetSetDef UIGridPointState::getsetters[] = { {"visible", (getter)UIGridPointState::get_bool_member, (setter)UIGridPointState::set_bool_member, "Is the GridPointState visible", (void*)0}, {"discovered", (getter)UIGridPointState::get_bool_member, (setter)UIGridPointState::set_bool_member, "Has the GridPointState been discovered", (void*)1}, + {"point", (getter)UIGridPointState::get_point, NULL, "GridPoint at this position (None if not discovered)", NULL}, {NULL} /* Sentinel */ }; @@ -232,7 +310,7 @@ int UIGridPoint::setattro(PyUIGridPointObject* self, PyObject* name, PyObject* v sf::Color color = PyObject_to_sfColor(value); if (PyErr_Occurred()) return -1; color_layer->at(x, y) = color; - color_layer->markDirty(); + color_layer->markDirty(x, y); // Mark only the affected chunk return 0; } else if (layer->type == GridLayerType::Tile) { auto tile_layer = std::static_pointer_cast(layer); @@ -241,7 +319,7 @@ int UIGridPoint::setattro(PyUIGridPointObject* self, PyObject* name, PyObject* v return -1; } tile_layer->at(x, y) = PyLong_AsLong(value); - tile_layer->markDirty(); + tile_layer->markDirty(x, y); // Mark only the affected chunk return 0; } diff --git a/src/UIGridPoint.h b/src/UIGridPoint.h index 1eee314..16b01a4 100644 --- a/src/UIGridPoint.h +++ b/src/UIGridPoint.h @@ -31,6 +31,7 @@ typedef struct { UIGridPointState* data; std::shared_ptr grid; std::shared_ptr entity; + int x, y; // Position in grid (needed for .point property) } PyUIGridPointStateObject; // UIGridPoint - grid cell data for pathfinding and layer access @@ -49,6 +50,9 @@ public: static PyObject* get_bool_member(PyUIGridPointObject* self, void* closure); static PyObject* repr(PyUIGridPointObject* self); + // #114 - entities property: list of entities at this cell + static PyObject* get_entities(PyUIGridPointObject* self, void* closure); + // #150 - Dynamic property access for named layers static PyObject* getattro(PyUIGridPointObject* self, PyObject* name); static int setattro(PyUIGridPointObject* self, PyObject* name, PyObject* value); @@ -64,6 +68,9 @@ public: static int set_bool_member(PyUIGridPointStateObject* self, PyObject* value, void* closure); static PyGetSetDef getsetters[]; static PyObject* repr(PyUIGridPointStateObject* self); + + // #16 - point property: access to GridPoint (None if not discovered) + static PyObject* get_point(PyUIGridPointStateObject* self, void* closure); }; namespace mcrfpydef { diff --git a/tests/unit/test_gridpoint_entities.py b/tests/unit/test_gridpoint_entities.py new file mode 100644 index 0000000..b4ba1f2 --- /dev/null +++ b/tests/unit/test_gridpoint_entities.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Test GridPoint.entities property (#114) +======================================== + +Tests the GridPoint.entities property that returns a list of entities +at that grid cell position. +""" + +import mcrfpy +import sys + +def run_tests(): + """Run GridPoint.entities tests""" + print("=== GridPoint.entities Tests ===\n") + + # Test 1: Basic entity listing + print("Test 1: Basic entity listing") + grid = mcrfpy.Grid(pos=(0, 0), size=(640, 400), grid_size=(40, 25)) + + # Add entities at various positions + e1 = mcrfpy.Entity((5, 5)) + e2 = mcrfpy.Entity((5, 5)) # Same position as e1 + e3 = mcrfpy.Entity((10, 10)) + grid.entities.append(e1) + grid.entities.append(e2) + grid.entities.append(e3) + + # Check entities at (5, 5) + pt = grid.at(5, 5) + entities_at_5_5 = pt.entities + assert len(entities_at_5_5) == 2, f"Expected 2 entities at (5,5), got {len(entities_at_5_5)}" + print(f" Found {len(entities_at_5_5)} entities at (5, 5)") + + # Check entities at (10, 10) + pt2 = grid.at(10, 10) + entities_at_10_10 = pt2.entities + assert len(entities_at_10_10) == 1, f"Expected 1 entity at (10,10), got {len(entities_at_10_10)}" + print(f" Found {len(entities_at_10_10)} entity at (10, 10)") + + # Check empty cell + pt3 = grid.at(0, 0) + entities_at_0_0 = pt3.entities + assert len(entities_at_0_0) == 0, f"Expected 0 entities at (0,0), got {len(entities_at_0_0)}" + print(f" Found {len(entities_at_0_0)} entities at (0, 0)") + print() + + # Test 2: Entity references are valid + print("Test 2: Entity references are valid") + for e in pt.entities: + assert e.x == 5.0, f"Entity x should be 5.0, got {e.x}" + assert e.y == 5.0, f"Entity y should be 5.0, got {e.y}" + print(" All entity references have correct positions") + print() + + # Test 3: Entity movement updates listing + print("Test 3: Entity movement updates listing") + e1.x = 20 + e1.y = 20 + + # Old position should have one fewer entity + entities_at_5_5_after = grid.at(5, 5).entities + assert len(entities_at_5_5_after) == 1, f"Expected 1 entity at (5,5) after move, got {len(entities_at_5_5_after)}" + print(f" After moving e1: {len(entities_at_5_5_after)} entity at (5, 5)") + + # New position should have the moved entity + entities_at_20_20 = grid.at(20, 20).entities + assert len(entities_at_20_20) == 1, f"Expected 1 entity at (20,20), got {len(entities_at_20_20)}" + print(f" After moving e1: {len(entities_at_20_20)} entity at (20, 20)") + print() + + # Test 4: Multiple grids are independent + print("Test 4: Multiple grids are independent") + grid2 = mcrfpy.Grid(pos=(0, 0), size=(640, 400), grid_size=(40, 25)) + e4 = mcrfpy.Entity((5, 5)) + grid2.entities.append(e4) + + # Original grid should not see grid2's entity + assert len(grid.at(5, 5).entities) == 1, "Original grid should still have 1 entity at (5,5)" + assert len(grid2.at(5, 5).entities) == 1, "Second grid should have 1 entity at (5,5)" + print(" Grids maintain independent entity lists") + print() + + print("=== All GridPoint.entities Tests Passed! ===") + return True + +# Main execution +if __name__ == "__main__": + try: + if run_tests(): + print("\nPASS") + sys.exit(0) + else: + print("\nFAIL") + sys.exit(1) + except Exception as e: + print(f"\nFAIL: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/unit/test_gridpointstate_point.py b/tests/unit/test_gridpointstate_point.py new file mode 100644 index 0000000..348b5c7 --- /dev/null +++ b/tests/unit/test_gridpointstate_point.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Test GridPointState.point property (#16) +========================================= + +Tests the GridPointState.point property that provides access to the +GridPoint data from an entity's perspective. + +Key behavior: +- Returns None if the cell has not been discovered by the entity +- Returns the GridPoint (live reference) if discovered +- Works with the FOV/visibility system +""" + +import mcrfpy +import sys + +def run_tests(): + """Run GridPointState.point tests""" + print("=== GridPointState.point Tests ===\n") + + # Test 1: Undiscovered cell returns None + print("Test 1: Undiscovered cell returns None") + grid = mcrfpy.Grid(pos=(0, 0), size=(640, 400), grid_size=(40, 25)) + + # Set up grid + for y in range(25): + for x in range(40): + pt = grid.at(x, y) + pt.walkable = True + pt.transparent = True + + # Create entity + entity = mcrfpy.Entity((5, 5)) + grid.entities.append(entity) + + # Before update_visibility, nothing is discovered + state = entity.at((10, 10)) + assert state.point is None, f"Expected None for undiscovered cell, got {state.point}" + print(" Undiscovered cell returns None") + print() + + # Test 2: Discovered cell returns GridPoint + print("Test 2: Discovered cell returns GridPoint") + grid.fov = mcrfpy.FOV.SHADOW + grid.fov_radius = 8 + + entity.update_visibility() + + # Entity's own position should be discovered + state_own = entity.at((5, 5)) + assert state_own.discovered == True, "Entity's position should be discovered" + assert state_own.point is not None, "Discovered cell should return GridPoint" + print(f" Entity position: discovered={state_own.discovered}, point={state_own.point}") + print() + + # Test 3: GridPoint access through state + print("Test 3: GridPoint properties accessible through state.point") + + # Make a specific cell have known properties + grid.at(6, 5).walkable = False + grid.at(6, 5).transparent = True + + state_adj = entity.at((6, 5)) + assert state_adj.point is not None, "Adjacent cell should be discovered" + assert state_adj.point.walkable == False, f"Expected walkable=False, got {state_adj.point.walkable}" + assert state_adj.point.transparent == True, f"Expected transparent=True, got {state_adj.point.transparent}" + print(f" Adjacent cell (6,5): walkable={state_adj.point.walkable}, transparent={state_adj.point.transparent}") + print() + + # Test 4: Far cells remain undiscovered + print("Test 4: Cells outside FOV radius remain undiscovered") + + # Cell far from entity (outside radius 8) + state_far = entity.at((30, 20)) + assert state_far.discovered == False, "Far cell should not be discovered" + assert state_far.point is None, "Far cell point should be None" + print(f" Far cell (30,20): discovered={state_far.discovered}, point={state_far.point}") + print() + + # Test 5: Discovered but not visible cells + print("Test 5: Discovered but not currently visible cells") + + # Move entity to discover new area + entity.x = 6 + entity.y = 5 + entity.update_visibility() + + # Move back - old visible cells should now be discovered but not visible + entity.x = 5 + entity.y = 5 + entity.update_visibility() + + # A cell that was visible when at (6,5) but might not be visible from (5,5) + # Actually with radius 8, most nearby cells will still be visible + # Let's check a cell that's on the edge + state_edge = entity.at((12, 5)) # 7 cells away, should be visible with radius 8 + if state_edge.discovered: + assert state_edge.point is not None, "Discovered cell should have point access" + print(f" Edge cell (12,5): discovered={state_edge.discovered}, visible={state_edge.visible}") + print(f" point.walkable={state_edge.point.walkable}") + print() + + # Test 6: GridPoint.entities through state.point + print("Test 6: GridPoint.entities accessible through state.point") + + # Add another entity at a visible position + e2 = mcrfpy.Entity((7, 5)) + grid.entities.append(e2) + + state_with_entity = entity.at((7, 5)) + assert state_with_entity.point is not None, "Cell should be discovered" + entities_at_cell = state_with_entity.point.entities + assert len(entities_at_cell) == 1, f"Expected 1 entity, got {len(entities_at_cell)}" + print(f" Cell (7,5) has {len(entities_at_cell)} entity via state.point.entities") + print() + + # Test 7: Live reference - changes to GridPoint are reflected + print("Test 7: state.point is a live reference") + + # Get state before change + state_live = entity.at((8, 5)) + original_walkable = state_live.point.walkable + + # Change the actual GridPoint + grid.at(8, 5).walkable = not original_walkable + + # Check that state.point reflects the change + new_walkable = state_live.point.walkable + assert new_walkable != original_walkable, "state.point should reflect GridPoint changes" + print(f" Changed walkable from {original_walkable} to {new_walkable} - reflected in state.point") + print() + + # Test 8: Wall blocking visibility + print("Test 8: Walls block visibility correctly") + + # Create a wall + grid.at(15, 5).transparent = False + grid.at(15, 5).walkable = False + + entity.update_visibility() + + # Cell behind wall should not be visible (and possibly not discovered) + state_behind = entity.at((20, 5)) # Behind wall at x=15 + print(f" Cell behind wall (20,5): visible={state_behind.visible}, discovered={state_behind.discovered}") + if not state_behind.discovered: + assert state_behind.point is None, "Undiscovered cell behind wall should have point=None" + print(" Correctly returns None for undiscovered cell behind wall") + print() + + print("=== All GridPointState.point Tests Passed! ===") + return True + +# Main execution +if __name__ == "__main__": + try: + if run_tests(): + print("\nPASS") + sys.exit(0) + else: + print("\nFAIL") + sys.exit(1) + except Exception as e: + print(f"\nFAIL: {e}") + import traceback + traceback.print_exc() + sys.exit(1)