Spaces:
Running
Running
| """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, | |
| ) | |
| 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 | |
| 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) | |