McRogueFace/tests/unit/perspective_map_test.py

112 lines
3.6 KiB
Python
Raw Normal View History

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
"""
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()