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>
This commit is contained in:
parent
417fc43325
commit
f797120d53
24 changed files with 7998 additions and 1906 deletions
112
tests/unit/perspective_map_test.py
Normal file
112
tests/unit/perspective_map_test.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
"""
|
||||
Tests for Entity.perspective_map (#294).
|
||||
|
||||
Covers:
|
||||
- Lazy allocation (None when no grid; allocated on first access with a grid)
|
||||
- Identity: multiple accesses share the same underlying DiscreteMap
|
||||
- Three-state values: 0=UNKNOWN, 1=DISCOVERED, 2=VISIBLE
|
||||
- perspective_map[x, y] returns Perspective enum members
|
||||
- visible subset discovered invariant after updateVisibility
|
||||
- entity.at() returns GridPoint when VISIBLE, None otherwise
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
|
||||
def fail(msg):
|
||||
print(f"FAIL: {msg}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
scene = mcrfpy.Scene("perspective_map_test")
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
# Entity without a grid -> perspective_map is None
|
||||
orphan = mcrfpy.Entity(grid_pos=(0, 0))
|
||||
if orphan.perspective_map is not None:
|
||||
fail("entity without grid should have perspective_map == None")
|
||||
|
||||
# Attach to a grid; perspective_map lazy-allocates on first access
|
||||
grid = mcrfpy.Grid(grid_size=(12, 9))
|
||||
scene.children.append(grid)
|
||||
e = mcrfpy.Entity(grid_pos=(4, 3))
|
||||
grid.entities.append(e)
|
||||
|
||||
pm = e.perspective_map
|
||||
if pm is None:
|
||||
fail("perspective_map should be allocated once entity has a grid")
|
||||
if pm.size != (12, 9):
|
||||
fail(f"perspective_map size should match grid size, got {pm.size}")
|
||||
|
||||
# Initial state: all UNKNOWN
|
||||
for y in range(9):
|
||||
for x in range(12):
|
||||
if int(pm[x, y]) != 0:
|
||||
fail(f"initial pm[{x},{y}] should be 0, got {pm[x, y]}")
|
||||
|
||||
# Identity: a second access wraps the same shared DiscreteMap.
|
||||
# Mutating through one is visible through the other.
|
||||
pm2 = e.perspective_map
|
||||
pm2[1, 1] = 2
|
||||
if int(pm[1, 1]) != 2:
|
||||
fail("perspective_map accesses should share underlying buffer")
|
||||
|
||||
# Values returned as Perspective enum members (IntEnum).
|
||||
pm[2, 2] = 1
|
||||
val = pm[2, 2]
|
||||
if val != mcrfpy.Perspective.DISCOVERED:
|
||||
fail(f"pm[2,2] should equal Perspective.DISCOVERED, got {val!r}")
|
||||
if int(val) != 1:
|
||||
fail(f"IntEnum comparison failed: int(pm[2,2]) = {int(val)}")
|
||||
|
||||
# updateVisibility yields VISIBLE at entity's cell, UNKNOWN far away
|
||||
# (beyond FOV radius). We first wipe whatever leaked in.
|
||||
pm.fill(0)
|
||||
e.update_visibility()
|
||||
at_entity = pm[4, 3]
|
||||
if at_entity != mcrfpy.Perspective.VISIBLE:
|
||||
fail(f"entity's cell should be VISIBLE after update_visibility, got {at_entity}")
|
||||
|
||||
# Invariant: any VISIBLE cell remains at least DISCOVERED after the entity
|
||||
# moves away and we re-update.
|
||||
visible_before = set()
|
||||
for y in range(9):
|
||||
for x in range(12):
|
||||
if int(pm[x, y]) == 2:
|
||||
visible_before.add((x, y))
|
||||
|
||||
e.grid_pos = (0, 0) # move far corner
|
||||
e.update_visibility()
|
||||
|
||||
for (x, y) in visible_before:
|
||||
v = int(pm[x, y])
|
||||
if v < 1:
|
||||
fail(f"cell ({x},{y}) was VISIBLE, should now be at least DISCOVERED (>=1), got {v}")
|
||||
|
||||
# entity.at(): VISIBLE -> GridPoint; else None.
|
||||
p = e.at(0, 0)
|
||||
if p is None:
|
||||
fail("entity.at(own cell) should return GridPoint (VISIBLE)")
|
||||
if not hasattr(p, "walkable"):
|
||||
fail("entity.at(visible) should return GridPoint with .walkable")
|
||||
|
||||
# Find an UNKNOWN cell and verify at() returns None.
|
||||
found = False
|
||||
for y in range(9):
|
||||
for x in range(12):
|
||||
if int(pm[x, y]) == 0:
|
||||
if e.at(x, y) is not None:
|
||||
fail(f"entity.at({x},{y}) on UNKNOWN cell should return None")
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
# (Not fatal if no UNKNOWN cell exists — small grid + large FOV radius.)
|
||||
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue