midnight_mystery / tests /test_case_binding.py
lcpizani's picture
Add full app: pages, assets, backend, config
811d960
Raw
History Blame Contribute Delete
3.77 kB
"""Unit tests for case↔room binding (no LLM required).
Exercises `_assemble_case` directly with stub drafts to prove the binding never
emits an invalid room id, binds to the scene manifest's floorplan, and carries
human occupations — even when the (simulated) model returns garbage zones.
"""
from backend.services import case_generator as cg
from backend.models.scene_map import SceneManifest, SceneZone
def _manifest():
"""A small synthetic scene manifest (the static-map replacement for the
old procedural house generator)."""
return SceneManifest(
scene_id="test_scene", background="assets/maps/test/background.png",
tile_px=32, grid_w=10, grid_h=10,
zones=[
SceneZone(id="kitchen", label="Kitchen", rect=[0, 0, 5, 5],
spawn_points=[[1, 1]], clue_anchors=[[2, 2]]),
SceneZone(id="study", label="Study", rect=[5, 0, 5, 5],
spawn_points=[[6, 1]], clue_anchors=[[7, 2]]),
SceneZone(id="hall", label="Hall", rect=[0, 5, 5, 5],
spawn_points=[[1, 6]], clue_anchors=[[2, 7]]),
],
doors=[], collision=[[0] * 10 for _ in range(10)],
)
def _story_data():
return {
"method": "Poisoned tea",
"motive": "Greed",
"victim_raw": "Gerald Hoffmann, 52",
"opening_narration": "Something was wrong.",
"culprit_lying_skill": 8,
"timeline": [{"time": "21:00", "event": "tea"}],
}
def _drafts(n):
roles = ["culprit"] + ["witness"] * (n - 1)
return [
{"soul": f"## YOUR ALIBI\nI was home.\n", "role": roles[i],
"name": f"NPC{i}", "idx": i, "occupation": "butler"}
for i in range(n)
]
def test_assemble_binds_to_floorplan_and_roster():
manifest = _manifest()
fp = manifest.to_floorplan()
room_ids = manifest.zone_ids
n = 4
spawn_by_idx = [room_ids[i % len(room_ids)] for i in range(n)]
# Clues with deliberately invalid zones — must be remapped to real room ids.
clues_raw = [
{"id": "clue_01", "name": "Cup", "description": "x", "zone": "not_a_room",
"tile_coords": [3, 3], "implicates": ["npc_00"], "is_red_herring": False},
{"id": "clue_02", "name": "Bottle", "description": "y", "zone": room_ids[2],
"tile_coords": [4, 4], "implicates": ["npc_00"], "is_red_herring": False},
]
case = cg._assemble_case(_story_data(), _drafts(n), clues_raw, {}, "",
"medium", fp, spawn_by_idx, scene="test_scene",
victim_name="Gerald Hoffmann",
victim_description="The master of the house.")
valid = set(room_ids)
for npc in case.npcs:
assert npc.spawn_zone in valid, npc.spawn_zone
assert npc.occupation, "occupation must be set" # human occupation carried
for clue in case.clues:
assert clue.zone in valid, clue.zone # invalid zone repaired
assert case.floorplan is fp
assert case.scene == "test_scene"
assert case.victim.name == "Gerald Hoffmann" # player-supplied victim
assert case.zones == room_ids # zones synced to floorplan
def test_seed_case_is_internally_consistent():
sc = cg.SEED_CASE
assert sc.floorplan is not None
ids = {r.id for r in sc.floorplan.rooms}
assert sc.zones == [r.id for r in sc.floorplan.rooms]
for npc in sc.npcs:
assert npc.spawn_zone in ids
for clue in sc.clues:
assert clue.zone in ids
if __name__ == "__main__":
for fn in [test_assemble_binds_to_floorplan_and_roster,
test_seed_case_is_internally_consistent]:
fn()
print(f"PASS {fn.__name__}")