feat: Add entity.visible_entities() and improve entity.updateVisibility() (closes #113)
Phase 3 of Agent POV Integration: Entity.updateVisibility() improvements: - Now uses grid.fov_algorithm and grid.fov_radius instead of hardcoded values - Updates any ColorLayers bound to this entity via apply_perspective() - Properly triggers layer FOV recomputation when entity moves New Entity.visible_entities(fov=None, radius=None) method: - Returns list of other entities visible from this entity's position - Optional fov parameter to override grid's FOV algorithm - Optional radius parameter to override grid's fov_radius - Useful for AI decision-making and line-of-sight checks Test coverage in test_perspective_binding.py: - Tests entity movement with bound layers - Tests visible_entities with wall occlusion - Tests radius override limiting visibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
018e73590f
commit
c5b4200dea
161
src/UIEntity.cpp
161
src/UIEntity.cpp
|
|
@ -5,6 +5,7 @@
|
||||||
#include "PyObjectUtils.h"
|
#include "PyObjectUtils.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
|
#include "PyFOV.h"
|
||||||
// UIDrawable methods now in UIBase.h
|
// UIDrawable methods now in UIBase.h
|
||||||
#include "UIEntityPyMethods.h"
|
#include "UIEntityPyMethods.h"
|
||||||
|
|
||||||
|
|
@ -28,7 +29,7 @@ UIEntity::~UIEntity() {
|
||||||
void UIEntity::updateVisibility()
|
void UIEntity::updateVisibility()
|
||||||
{
|
{
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
// Lazy initialize gridstate if needed
|
// Lazy initialize gridstate if needed
|
||||||
if (gridstate.size() == 0) {
|
if (gridstate.size() == 0) {
|
||||||
gridstate.resize(grid->grid_x * grid->grid_y);
|
gridstate.resize(grid->grid_x * grid->grid_y);
|
||||||
|
|
@ -38,19 +39,19 @@ void UIEntity::updateVisibility()
|
||||||
state.discovered = false;
|
state.discovered = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, mark all cells as not visible
|
// First, mark all cells as not visible
|
||||||
for (auto& state : gridstate) {
|
for (auto& state : gridstate) {
|
||||||
state.visible = false;
|
state.visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute FOV from entity's position
|
// Compute FOV from entity's position using grid's FOV settings (#114)
|
||||||
int x = static_cast<int>(position.x);
|
int x = static_cast<int>(position.x);
|
||||||
int y = static_cast<int>(position.y);
|
int y = static_cast<int>(position.y);
|
||||||
|
|
||||||
// Use default FOV radius of 10 (can be made configurable later)
|
// Use grid's configured FOV algorithm and radius
|
||||||
grid->computeFOV(x, y, 10);
|
grid->computeFOV(x, y, grid->fov_radius, true, grid->fov_algorithm);
|
||||||
|
|
||||||
// Update visible cells based on FOV computation
|
// Update visible cells based on FOV computation
|
||||||
for (int gy = 0; gy < grid->grid_y; gy++) {
|
for (int gy = 0; gy < grid->grid_y; gy++) {
|
||||||
for (int gx = 0; gx < grid->grid_x; gx++) {
|
for (int gx = 0; gx < grid->grid_x; gx++) {
|
||||||
|
|
@ -61,6 +62,32 @@ void UIEntity::updateVisibility()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #113 - Update any ColorLayers bound to this entity via perspective
|
||||||
|
// Get shared_ptr to self for comparison
|
||||||
|
std::shared_ptr<UIEntity> self_ptr = nullptr;
|
||||||
|
if (grid->entities) {
|
||||||
|
for (auto& entity : *grid->entities) {
|
||||||
|
if (entity.get() == this) {
|
||||||
|
self_ptr = entity;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self_ptr) {
|
||||||
|
for (auto& layer : grid->layers) {
|
||||||
|
if (layer->type == GridLayerType::Color) {
|
||||||
|
auto color_layer = std::static_pointer_cast<ColorLayer>(layer);
|
||||||
|
if (color_layer->has_perspective) {
|
||||||
|
auto bound_entity = color_layer->perspective_entity.lock();
|
||||||
|
if (bound_entity && bound_entity.get() == this) {
|
||||||
|
color_layer->updatePerspective();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
|
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
|
||||||
|
|
@ -588,11 +615,101 @@ PyObject* UIEntity::update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSE
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
static const char* keywords[] = {"fov", "radius", nullptr};
|
||||||
|
PyObject* fov_arg = nullptr;
|
||||||
|
int radius = -1; // -1 means use grid default
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oi", const_cast<char**>(keywords),
|
||||||
|
&fov_arg, &radius)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if entity has a grid
|
||||||
|
if (!self->data || !self->data->grid) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to find visible entities");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto grid = self->data->grid;
|
||||||
|
|
||||||
|
// Parse FOV algorithm - use grid default if not specified
|
||||||
|
TCOD_fov_algorithm_t algorithm = grid->fov_algorithm;
|
||||||
|
bool fov_was_none = false;
|
||||||
|
if (fov_arg && fov_arg != Py_None) {
|
||||||
|
if (PyFOV::from_arg(fov_arg, &algorithm, &fov_was_none) < 0) {
|
||||||
|
return NULL; // Error already set
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use grid radius if not specified
|
||||||
|
if (radius < 0) {
|
||||||
|
radius = grid->fov_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current position
|
||||||
|
int x = static_cast<int>(self->data->position.x);
|
||||||
|
int y = static_cast<int>(self->data->position.y);
|
||||||
|
|
||||||
|
// Compute FOV from this entity's position
|
||||||
|
grid->computeFOV(x, y, radius, true, algorithm);
|
||||||
|
|
||||||
|
// Create result list
|
||||||
|
PyObject* result = PyList_New(0);
|
||||||
|
if (!result) return PyErr_NoMemory();
|
||||||
|
|
||||||
|
// Get Entity type for creating Python objects
|
||||||
|
PyTypeObject* entity_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
||||||
|
if (!entity_type) {
|
||||||
|
Py_DECREF(result);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through all entities in the grid
|
||||||
|
if (grid->entities) {
|
||||||
|
for (auto& entity : *grid->entities) {
|
||||||
|
// Skip self
|
||||||
|
if (entity.get() == self->data.get()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if entity is in FOV
|
||||||
|
int ex = static_cast<int>(entity->position.x);
|
||||||
|
int ey = static_cast<int>(entity->position.y);
|
||||||
|
|
||||||
|
if (grid->isInFOV(ex, ey)) {
|
||||||
|
// Create Python Entity object for this entity
|
||||||
|
auto pyEntity = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0);
|
||||||
|
if (!pyEntity) {
|
||||||
|
Py_DECREF(result);
|
||||||
|
Py_DECREF(entity_type);
|
||||||
|
return PyErr_NoMemory();
|
||||||
|
}
|
||||||
|
|
||||||
|
pyEntity->data = entity;
|
||||||
|
pyEntity->weakreflist = NULL;
|
||||||
|
|
||||||
|
if (PyList_Append(result, (PyObject*)pyEntity) < 0) {
|
||||||
|
Py_DECREF(pyEntity);
|
||||||
|
Py_DECREF(result);
|
||||||
|
Py_DECREF(entity_type);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
Py_DECREF(pyEntity); // List now owns the reference
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_DECREF(entity_type);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
PyMethodDef UIEntity::methods[] = {
|
PyMethodDef UIEntity::methods[] = {
|
||||||
{"at", (PyCFunction)UIEntity::at, METH_O},
|
{"at", (PyCFunction)UIEntity::at, METH_O},
|
||||||
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
||||||
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
||||||
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
|
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
|
||||||
"path_to(x: int, y: int) -> bool\n\n"
|
"path_to(x: int, y: int) -> bool\n\n"
|
||||||
"Find and follow path to target position using A* pathfinding.\n\n"
|
"Find and follow path to target position using A* pathfinding.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
|
|
@ -602,12 +719,22 @@ PyMethodDef UIEntity::methods[] = {
|
||||||
" True if a path was found and the entity started moving, False otherwise\n\n"
|
" True if a path was found and the entity started moving, False otherwise\n\n"
|
||||||
"The entity will automatically move along the path over multiple frames.\n"
|
"The entity will automatically move along the path over multiple frames.\n"
|
||||||
"Call this again to change the target or repath."},
|
"Call this again to change the target or repath."},
|
||||||
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
|
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
|
||||||
"update_visibility() -> None\n\n"
|
"update_visibility() -> None\n\n"
|
||||||
"Update entity's visibility state based on current FOV.\n\n"
|
"Update entity's visibility state based on current FOV.\n\n"
|
||||||
"Recomputes which cells are visible from the entity's position and updates\n"
|
"Recomputes which cells are visible from the entity's position and updates\n"
|
||||||
"the entity's gridstate to track explored areas. This is called automatically\n"
|
"the entity's gridstate to track explored areas. This is called automatically\n"
|
||||||
"when the entity moves if it has a grid with perspective set."},
|
"when the entity moves if it has a grid with perspective set."},
|
||||||
|
{"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"visible_entities(fov=None, radius=None) -> list[Entity]\n\n"
|
||||||
|
"Get list of other entities visible from this entity's position.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" fov (FOV, optional): FOV algorithm to use. Default: grid.fov\n"
|
||||||
|
" radius (int, optional): FOV radius. Default: grid.fov_radius\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" List of Entity objects that are within field of view.\n\n"
|
||||||
|
"Computes FOV from this entity's position and returns all other entities\n"
|
||||||
|
"whose positions fall within the visible area."},
|
||||||
{NULL, NULL, 0, NULL}
|
{NULL, NULL, 0, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -620,7 +747,7 @@ PyMethodDef UIEntity_all_methods[] = {
|
||||||
{"at", (PyCFunction)UIEntity::at, METH_O},
|
{"at", (PyCFunction)UIEntity::at, METH_O},
|
||||||
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
||||||
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
||||||
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
|
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
|
||||||
"path_to(x: int, y: int) -> bool\n\n"
|
"path_to(x: int, y: int) -> bool\n\n"
|
||||||
"Find and follow path to target position using A* pathfinding.\n\n"
|
"Find and follow path to target position using A* pathfinding.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
|
|
@ -630,12 +757,22 @@ PyMethodDef UIEntity_all_methods[] = {
|
||||||
" True if a path was found and the entity started moving, False otherwise\n\n"
|
" True if a path was found and the entity started moving, False otherwise\n\n"
|
||||||
"The entity will automatically move along the path over multiple frames.\n"
|
"The entity will automatically move along the path over multiple frames.\n"
|
||||||
"Call this again to change the target or repath."},
|
"Call this again to change the target or repath."},
|
||||||
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
|
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
|
||||||
"update_visibility() -> None\n\n"
|
"update_visibility() -> None\n\n"
|
||||||
"Update entity's visibility state based on current FOV.\n\n"
|
"Update entity's visibility state based on current FOV.\n\n"
|
||||||
"Recomputes which cells are visible from the entity's position and updates\n"
|
"Recomputes which cells are visible from the entity's position and updates\n"
|
||||||
"the entity's gridstate to track explored areas. This is called automatically\n"
|
"the entity's gridstate to track explored areas. This is called automatically\n"
|
||||||
"when the entity moves if it has a grid with perspective set."},
|
"when the entity moves if it has a grid with perspective set."},
|
||||||
|
{"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"visible_entities(fov=None, radius=None) -> list[Entity]\n\n"
|
||||||
|
"Get list of other entities visible from this entity's position.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" fov (FOV, optional): FOV algorithm to use. Default: grid.fov\n"
|
||||||
|
" radius (int, optional): FOV radius. Default: grid.fov_radius\n\n"
|
||||||
|
"Returns:\n"
|
||||||
|
" List of Entity objects that are within field of view.\n\n"
|
||||||
|
"Computes FOV from this entity's position and returns all other entities\n"
|
||||||
|
"whose positions fall within the visible area."},
|
||||||
{NULL} // Sentinel
|
{NULL} // Sentinel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ public:
|
||||||
static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
||||||
static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
static PyObject* update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
||||||
|
static PyObject* visible_entities(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
||||||
static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
static PyObject* get_position(PyUIEntityObject* self, void* closure);
|
static PyObject* get_position(PyUIEntityObject* self, void* closure);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test Perspective Binding System
|
||||||
|
===============================
|
||||||
|
|
||||||
|
Tests the integration between:
|
||||||
|
1. ColorLayer.apply_perspective() - binding a layer to an entity
|
||||||
|
2. entity.updateVisibility() - automatically updating bound layers
|
||||||
|
3. ColorLayer.clear_perspective() - removing the binding
|
||||||
|
|
||||||
|
This implements issue #113 requirements for "Agent POV Integration".
|
||||||
|
"""
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def run_tests():
|
||||||
|
"""Run perspective binding tests"""
|
||||||
|
print("=== Perspective Binding Tests ===\n")
|
||||||
|
|
||||||
|
# Test 1: Create grid with entity and color layer
|
||||||
|
print("Test 1: Setup")
|
||||||
|
grid = mcrfpy.Grid(pos=(0, 0), size=(640, 400), grid_size=(40, 25))
|
||||||
|
|
||||||
|
# Set up walls
|
||||||
|
for y in range(25):
|
||||||
|
for x in range(40):
|
||||||
|
point = grid.at(x, y)
|
||||||
|
# Border walls
|
||||||
|
if x == 0 or x == 39 or y == 0 or y == 24:
|
||||||
|
point.walkable = False
|
||||||
|
point.transparent = False
|
||||||
|
# Central wall
|
||||||
|
elif x == 20 and y != 12: # Wall with door at y=12
|
||||||
|
point.walkable = False
|
||||||
|
point.transparent = False
|
||||||
|
else:
|
||||||
|
point.walkable = True
|
||||||
|
point.transparent = True
|
||||||
|
|
||||||
|
# Create player entity
|
||||||
|
player = mcrfpy.Entity((5, 12))
|
||||||
|
grid.entities.append(player)
|
||||||
|
print(f" Player at ({player.x}, {player.y})")
|
||||||
|
print(" Grid setup complete")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 2: Apply perspective binding
|
||||||
|
print("Test 2: Perspective Binding")
|
||||||
|
fov_layer = grid.add_layer('color', z_index=-1)
|
||||||
|
fov_layer.fill((0, 0, 0, 255)) # Start with black (unknown)
|
||||||
|
|
||||||
|
fov_layer.apply_perspective(
|
||||||
|
entity=player,
|
||||||
|
visible=(255, 255, 200, 64),
|
||||||
|
discovered=(100, 100, 100, 128),
|
||||||
|
unknown=(0, 0, 0, 255)
|
||||||
|
)
|
||||||
|
print(" Applied perspective to layer")
|
||||||
|
|
||||||
|
# Check layer is bound
|
||||||
|
# (We can't directly check internal state, but we can verify behavior)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 3: updateVisibility should update the bound layer
|
||||||
|
print("Test 3: Entity updateVisibility")
|
||||||
|
player.update_visibility()
|
||||||
|
|
||||||
|
# Check that the player's position is now visible
|
||||||
|
visible_cell = fov_layer.at(int(player.x), int(player.y))
|
||||||
|
assert visible_cell.r == 255, f"Player position should be visible (got r={visible_cell.r})"
|
||||||
|
print(" Player position has visible color after updateVisibility()")
|
||||||
|
|
||||||
|
# Check that cells behind wall are unknown
|
||||||
|
behind_wall = fov_layer.at(21, 5)
|
||||||
|
assert behind_wall.r == 0, f"Behind wall should be unknown (got r={behind_wall.r})"
|
||||||
|
print(" Cell behind wall is unknown")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 4: Moving entity and calling updateVisibility
|
||||||
|
print("Test 4: Entity Movement with Perspective")
|
||||||
|
|
||||||
|
# Move player through the door
|
||||||
|
player.x = 21
|
||||||
|
player.y = 12
|
||||||
|
player.update_visibility()
|
||||||
|
|
||||||
|
# Now the player should see both sides of the wall
|
||||||
|
# Check a cell that was previously hidden
|
||||||
|
now_visible = fov_layer.at(25, 12) # To the right of where player moved
|
||||||
|
# This should now be visible (or discovered if was visible)
|
||||||
|
assert now_visible.r in [255, 100], f"Cell should be visible or discovered (got r={now_visible.r})"
|
||||||
|
print(f" After moving to door, cell (25,12) has r={now_visible.r}")
|
||||||
|
|
||||||
|
# Player's new position should be visible
|
||||||
|
new_pos_color = fov_layer.at(int(player.x), int(player.y))
|
||||||
|
assert new_pos_color.r == 255, f"New player position should be visible (got r={new_pos_color.r})"
|
||||||
|
print(f" Player's new position ({player.x}, {player.y}) is visible")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 5: Check discovered cells remain discovered
|
||||||
|
print("Test 5: Discovered State Persistence")
|
||||||
|
|
||||||
|
# Move player away from original position
|
||||||
|
player.x = 35
|
||||||
|
player.y = 12
|
||||||
|
player.update_visibility()
|
||||||
|
|
||||||
|
# Original position (5, 12) should now be discovered (not visible, but was seen)
|
||||||
|
original_pos = fov_layer.at(5, 12)
|
||||||
|
# It could be visible if in line of sight, or discovered if not
|
||||||
|
print(f" Original position (5,12) color: r={original_pos.r}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 6: Clear perspective
|
||||||
|
print("Test 6: Clear Perspective")
|
||||||
|
fov_layer.clear_perspective()
|
||||||
|
|
||||||
|
# After clearing, updateVisibility should not affect this layer
|
||||||
|
fov_layer.fill((128, 0, 128, 255)) # Fill with purple
|
||||||
|
player.update_visibility()
|
||||||
|
|
||||||
|
# Layer should still be purple (not modified by updateVisibility)
|
||||||
|
check_cell = fov_layer.at(int(player.x), int(player.y))
|
||||||
|
assert check_cell.r == 128, f"Layer should be unchanged after clear_perspective (got r={check_cell.r})"
|
||||||
|
assert check_cell.g == 0, f"Layer should be unchanged (got g={check_cell.g})"
|
||||||
|
assert check_cell.b == 128, f"Layer should be unchanged (got b={check_cell.b})"
|
||||||
|
print(" Layer unchanged after clear_perspective()")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 7: Grid FOV settings
|
||||||
|
print("Test 7: Grid FOV Settings Integration")
|
||||||
|
|
||||||
|
# Create a new grid and layer to test FOV radius without discovered interference
|
||||||
|
grid2 = mcrfpy.Grid(pos=(0, 0), size=(640, 400), grid_size=(40, 25))
|
||||||
|
|
||||||
|
# Set all cells walkable/transparent
|
||||||
|
for y in range(25):
|
||||||
|
for x in range(40):
|
||||||
|
point = grid2.at(x, y)
|
||||||
|
point.walkable = True
|
||||||
|
point.transparent = True
|
||||||
|
|
||||||
|
# Create player entity
|
||||||
|
player2 = mcrfpy.Entity((20, 12))
|
||||||
|
grid2.entities.append(player2)
|
||||||
|
|
||||||
|
# Set grid FOV settings
|
||||||
|
grid2.fov = mcrfpy.FOV.SHADOW
|
||||||
|
grid2.fov_radius = 5 # Smaller radius
|
||||||
|
|
||||||
|
# Create layer and bind perspective
|
||||||
|
fov_layer2 = grid2.add_layer('color', z_index=-1)
|
||||||
|
fov_layer2.fill((0, 0, 0, 255)) # Start with black (unknown)
|
||||||
|
|
||||||
|
fov_layer2.apply_perspective(
|
||||||
|
entity=player2,
|
||||||
|
visible=(255, 0, 0, 64), # Red for visible
|
||||||
|
discovered=(100, 100, 100, 128),
|
||||||
|
unknown=(0, 0, 0, 255)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update visibility - this should only illuminate cells within radius 5
|
||||||
|
player2.update_visibility()
|
||||||
|
|
||||||
|
# With radius 5, cells far from player should be unknown (never discovered)
|
||||||
|
far_cell = fov_layer2.at(30, 12) # 10 cells away from player
|
||||||
|
assert far_cell.r == 0, f"Far cell should be unknown with radius 5 (got r={far_cell.r})"
|
||||||
|
print(f" Far cell (30,12) is unknown with radius=5")
|
||||||
|
|
||||||
|
# Near cell should be visible
|
||||||
|
near_cell = fov_layer2.at(22, 12) # 2 cells away
|
||||||
|
assert near_cell.r == 255, f"Near cell should be visible (got r={near_cell.r})"
|
||||||
|
print(f" Near cell (22,12) is visible")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 8: visible_entities method
|
||||||
|
print("Test 8: Entity.visible_entities()")
|
||||||
|
|
||||||
|
# Create a grid with multiple entities
|
||||||
|
grid3 = mcrfpy.Grid(pos=(0, 0), size=(640, 400), grid_size=(40, 25))
|
||||||
|
|
||||||
|
# Set all cells transparent
|
||||||
|
for y in range(25):
|
||||||
|
for x in range(40):
|
||||||
|
point = grid3.at(x, y)
|
||||||
|
point.walkable = True
|
||||||
|
point.transparent = True
|
||||||
|
|
||||||
|
# Add a wall to block visibility
|
||||||
|
for y in range(25):
|
||||||
|
if y != 12: # Door at y=12
|
||||||
|
point = grid3.at(20, y)
|
||||||
|
point.walkable = False
|
||||||
|
point.transparent = False
|
||||||
|
|
||||||
|
# Create entities
|
||||||
|
player3 = mcrfpy.Entity((5, 12)) # Left side
|
||||||
|
ally = mcrfpy.Entity((8, 12)) # Near player
|
||||||
|
enemy1 = mcrfpy.Entity((35, 12)) # Behind wall
|
||||||
|
enemy2 = mcrfpy.Entity((25, 12)) # Through door (should be visible)
|
||||||
|
|
||||||
|
grid3.entities.append(player3)
|
||||||
|
grid3.entities.append(ally)
|
||||||
|
grid3.entities.append(enemy1)
|
||||||
|
grid3.entities.append(enemy2)
|
||||||
|
|
||||||
|
# Set grid FOV settings
|
||||||
|
grid3.fov = mcrfpy.FOV.SHADOW
|
||||||
|
grid3.fov_radius = 20
|
||||||
|
|
||||||
|
# Get visible entities from player
|
||||||
|
visible = player3.visible_entities()
|
||||||
|
visible_positions = [(int(e.x), int(e.y)) for e in visible]
|
||||||
|
|
||||||
|
print(f" Player at (5, 12)")
|
||||||
|
print(f" Visible entities: {visible_positions}")
|
||||||
|
|
||||||
|
# Ally should be visible
|
||||||
|
assert (8, 12) in visible_positions, "Ally at (8,12) should be visible"
|
||||||
|
print(" Ally at (8, 12) is visible")
|
||||||
|
|
||||||
|
# Enemy1 behind wall should NOT be visible
|
||||||
|
assert (35, 12) not in visible_positions, "Enemy1 at (35,12) should NOT be visible (behind wall)"
|
||||||
|
print(" Enemy1 at (35, 12) is NOT visible (behind wall)")
|
||||||
|
|
||||||
|
# Enemy2 through door should be visible
|
||||||
|
assert (25, 12) in visible_positions, "Enemy2 at (25,12) should be visible through door"
|
||||||
|
print(" Enemy2 at (25, 12) is visible (through door)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Test 9: visible_entities with radius override
|
||||||
|
print("Test 9: visible_entities with radius override")
|
||||||
|
|
||||||
|
# With small radius, only ally should be visible
|
||||||
|
visible_small = player3.visible_entities(radius=4)
|
||||||
|
visible_small_positions = [(int(e.x), int(e.y)) for e in visible_small]
|
||||||
|
|
||||||
|
print(f" With radius=4: {visible_small_positions}")
|
||||||
|
assert (8, 12) in visible_small_positions, "Ally should be visible with radius=4"
|
||||||
|
assert (25, 12) not in visible_small_positions, "Enemy2 should NOT be visible with radius=4"
|
||||||
|
print(" Correctly limited visibility to nearby entities")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=== All Perspective Binding 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