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