feat: Add GridPoint.entities and GridPointState.point properties
GridPoint.entities (#114): - Returns list of entities at this grid cell position - Enables convenient cell-based entity queries without manual iteration - Example: grid.at(5, 5).entities → [<Entity>, <Entity>] GridPointState.point (#16): - Returns GridPoint if entity has discovered this cell, None otherwise - Respects entity's perspective: undiscovered cells return None - Enables entity.at(x,y).point.walkable style access - Live reference: changes to GridPoint are immediately visible This provides a simpler solution for #16 without the complexity of caching stale GridPoint copies. The visible/discovered flags indicate whether the entity "should" trust the data; Python can implement memory systems if needed. closes #114, closes #16 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a529e5eac3
commit
f33e79a123
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <cstring> // #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<int>(entity->position.x) == target_x &&
|
||||
static_cast<int>(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<int>(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<TileLayer>(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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ typedef struct {
|
|||
UIGridPointState* data;
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
std::shared_ptr<UIEntity> 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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue