case0 / tests /test_crime_kinds.py
HusseinEid's picture
feat: multi-crime cases, scene+exhibit pixel art, background AI generation
80cd1f2 verified
Raw
History Blame Contribute Delete
4.95 kB
"""Multi-crime-type cases: every kind assembles, solves, and projects with the right
display labels - and homicide stays the unchanged default for old case files.
"""
from __future__ import annotations
import pytest
from case_zero.api.case_adapter import casefile_to_public
from case_zero.generator.assemble import assemble_case
from case_zero.generator.crime_profiles import PROFILES, kind_for_seed, profile_for
from case_zero.generator.stages import GenSuspect, MysteryOut, WorldCastOut, mystery_prompt, world_cast_prompt
from case_zero.projections.suspect_brief import build_suspect_brief
from case_zero.schemas.case import GenerationKnobs
from case_zero.schemas.enums import CrimeKind, MotiveCategory
from case_zero.schemas.timeline import TimeWindow
from case_zero.solver.checker import check
def _world() -> WorldCastOut:
return WorldCastOut(
title="THE LANTERN ROOM",
briefing="Trouble at a winter salon.",
setting_name="Ashgrove House",
locations=["The Study", "The Parlor", "The Conservatory", "The Cellar"],
victim_name="Edmund Crane",
victim_role="Master Bookbinder",
cause_of_death="The loss came to light just before midnight.",
found_at_index=0,
weapon_name="bookbinding press",
weapon_kind="blunt",
suspects=[
GenSuspect(name="Cora Vale", role="Apprentice", persona_summary="Quiet, ink-stained.", secret="She forged a signature once.", cover_story="I was cataloguing.", evidence_name="ink bottle", evidence_reveal="A spilled bottle of rare ink.", gender="female"),
GenSuspect(name="Miles Ardent", role="Patron", persona_summary="Wealthy, indebted.", secret="He owes the victim money.", cover_story="I never left the salon.", evidence_name="promissory note", evidence_reveal="A note for a large sum, unpaid.", gender="male"),
GenSuspect(name="Greta Holt", role="Housekeeper", persona_summary="Long-serving.", secret="She skims the accounts.", cover_story="I was fetching wine.", evidence_name="ledger", evidence_reveal="A ledger with altered figures.", gender="female"),
],
)
def _mystery() -> MysteryOut:
return MysteryOut(
motive_category=MotiveCategory.GREED,
motive_summary="To erase a debt that would have ruined him.",
method_narrative="He waited until the study emptied.",
alibi_claim="I was in the parlor with the others all evening.",
alibi_claimed_loc_index=1,
breaker_one_name="thread",
breaker_one_reveal="A torn thread of dark cloth on the study latch.",
breaker_two_name="footprint",
breaker_two_reveal="A damp footprint by the press, fresh at the hour.",
deduction_chain=["The study was locked.", "One route stayed open.", "The thread matches."],
)
def _case(kind: CrimeKind):
return assemble_case(
case_id="gen-000099", seed=99, knobs=GenerationKnobs(crime_kind=kind),
world=_world(), mystery=_mystery(), profile=profile_for(kind),
window=TimeWindow(start_min=21 * 60, end_min=22 * 60),
tod=TimeWindow(start_min=21 * 60 + 20, end_min=21 * 60 + 50),
culprit_idx=1, crime_idx=0, claimed_idx=1,
)
@pytest.mark.parametrize("kind", list(CrimeKind))
def test_every_kind_assembles_solvable_and_labelled(kind: CrimeKind):
case = _case(kind)
assert case.crime_kind == kind
report = check(case)
assert report.ok, f"{kind}: {report.issues}"
pub = casefile_to_public(case)
profile = PROFILES[kind]
assert pub.kind == kind.value
assert pub.division == profile.division
assert pub.victim_status == profile.victim_status
assert ("VERDICT", profile.verdict) in pub.facts
# the incident story beat sits on the REAL crime scene, not a generic painter name
assert pub.story_beats[1].scene == pub.scene
@pytest.mark.parametrize("kind", list(CrimeKind))
def test_culprit_brief_states_the_right_deed(kind: CrimeKind):
case = _case(kind)
culprit = case.suspect(case.culprit.sus_id)
brief = build_suspect_brief(case, culprit)
deed = PROFILES[kind].brief_deed.format(
victim=case.victim.name, instrument=case.weapon.name, room="The Study",
)
assert any(deed in line for line in brief.i_must_conceal)
def test_prompts_carry_the_kind():
p = profile_for(CrimeKind.THEFT)
w = world_cast_prompt(p, "", "", "", 4, 21 * 60, 22 * 60)
assert "heist" in w and "STOLEN OBJECT" in w
m = mystery_prompt(p, "Miles Ardent", "Patron", "Edmund Crane", "the Falcon Diamond",
"The Study", "The Parlor", 21 * 60, 22 * 60)
assert "thief" in m and "stole" in m
def test_kind_for_seed_covers_all_kinds():
kinds = {kind_for_seed(s) for s in range(64)}
assert kinds == set(CrimeKind)
# homicide stays the most common draw
draws = [kind_for_seed(s) for s in range(90)]
assert draws.count(CrimeKind.HOMICIDE) > draws.count(CrimeKind.THEFT)