Spaces:
Running
Running
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55 | """Author the hand-crafted tutorial case and write cases/seeds/tutorial.json. | |
| The tutorial doubles as: the onboarding case that teaches the loop, a deterministic | |
| demo case, and a golden test fixture. It is fully solvable by construction; the | |
| solvability checker is run before it is written. | |
| """ | |
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ROOT / "src")) | |
| from case_zero.constants import SEED_CASES_DIR # noqa: E402 | |
| from case_zero.persistence.case_store import save_case # noqa: E402 | |
| from case_zero.schemas import ( # noqa: E402 | |
| AlibiLie, | |
| AlibiSegment, | |
| AnchoredLie, | |
| CaseFile, | |
| Clue, | |
| Culprit, | |
| Difficulty, | |
| DiscoveryMethod, | |
| Fact, | |
| GenerationKnobs, | |
| Location, | |
| Motive, | |
| MotiveCategory, | |
| PersonalityAxes, | |
| PhysicalCapability, | |
| Relationship, | |
| Setting, | |
| Solution, | |
| StatedAlibi, | |
| Suspect, | |
| TimeWindow, | |
| Victim, | |
| VisualDescriptor, | |
| Weapon, | |
| WhereaboutsSegment, | |
| ) | |
| from case_zero.schemas.enums import SubjectType # noqa: E402 | |
| def m(hour: int, minute: int) -> int: | |
| return hour * 60 + minute | |
| WINDOW = TimeWindow(start_min=m(21, 0), end_min=m(22, 0)) | |
| def _locations() -> tuple[Location, ...]: | |
| return ( | |
| Location(loc_id="L1", name="The Lounge", description="The main club floor.", | |
| adjacent_to=("L2", "L3", "L5")), | |
| Location(loc_id="L2", name="The Kitchen", adjacent_to=("L1",)), | |
| Location(loc_id="L3", name="The Terrace", description="The open rooftop stage.", | |
| adjacent_to=("L1",)), | |
| Location(loc_id="L4", name="The Office", description="Vane's private office.", | |
| adjacent_to=("L5",)), | |
| Location(loc_id="L5", name="The Cloakroom", adjacent_to=("L1", "L4")), | |
| ) | |
| def _facts() -> tuple[Fact, ...]: | |
| return ( | |
| Fact(fact_id="F1", statement="Margot was in the office during the murder window.", | |
| true_value=True, loc_id="L4", at_min=m(21, 30)), | |
| Fact(fact_id="F2", statement="Margot smokes Gauloises cigarettes.", true_value=True), | |
| Fact(fact_id="F3", statement="Eddie has been skimming from the till.", true_value=True), | |
| Fact(fact_id="F4", statement="Lillian was secretly in love with Cornelius.", true_value=True), | |
| Fact(fact_id="F5", statement="Boone falsified the club's ledgers.", true_value=True), | |
| ) | |
| def _clues() -> tuple[Clue, ...]: | |
| return ( | |
| Clue(clue_id="C1", name="Cloakroom ticket (9:48 PM)", | |
| reveal_text="A cloakroom claim ticket stamped 9:48 PM lies near the body - someone " | |
| "fetched a wrap mid-evening and ended up here.", | |
| discoverable_at_loc_id="L4", discovery_method=DiscoveryMethod.FORENSIC, | |
| supports_fact_id="F1", points_to_sus_id="S1", contradicts_alibi_of="S1", weight=1.0), | |
| Clue(clue_id="C2", name="Gauloises ash", | |
| reveal_text="Grey ash from a Gauloises cigarette on the office rug - an unusual brand.", | |
| discoverable_at_loc_id="L4", discovery_method=DiscoveryMethod.FORENSIC, | |
| supports_fact_id="F2", points_to_sus_id="S1", contradicts_alibi_of="S1", weight=0.7), | |
| Clue(clue_id="C3", name="Doctored bar tab", | |
| reveal_text="A bar ledger showing Eddie quietly forgiving his own tabs.", | |
| discoverable_at_loc_id="L1", discovery_method=DiscoveryMethod.DOCUMENT, | |
| supports_fact_id="F3", points_to_sus_id="S2", is_red_herring=True, weight=0.3), | |
| Clue(clue_id="C4", name="Torn love letter", | |
| reveal_text="A torn letter in Lillian's hand to Cornelius - equal parts love and fury.", | |
| discoverable_at_loc_id="L5", discovery_method=DiscoveryMethod.SEARCH, | |
| supports_fact_id="F4", points_to_sus_id="S3", is_red_herring=True, weight=0.3), | |
| Clue(clue_id="C5", name="Altered ledger page", | |
| reveal_text="A ledger page with figures scratched out and rewritten in Boone's hand.", | |
| discoverable_at_loc_id="L4", discovery_method=DiscoveryMethod.DOCUMENT, | |
| supports_fact_id="F5", points_to_sus_id="S4", is_red_herring=True, weight=0.3), | |
| ) | |
| def _visual(age: str, attire: str, mood: str, accent: str, gender: str) -> VisualDescriptor: | |
| return VisualDescriptor(subject_type=SubjectType.SUSPECT, palette="noir", age_band=age, | |
| attire=attire, mood=mood, accent_color=accent, gender=gender) | |
| def _suspects() -> tuple[Suspect, ...]: | |
| margot = Suspect( | |
| sus_id="S1", name="Margot Vane", role="the widow and co-owner", | |
| persona_summary="Poised and imperious in mourning black; clipped, controlled speech, and " | |
| "she resents every question.", | |
| is_culprit=True, | |
| physical_capability=PhysicalCapability(strength=True, mobility=True), | |
| personality=PersonalityAxes(composure=0.8, aggression=0.5, evasiveness=0.7), | |
| tells=("a too-steady voice", "smoothing her gloves"), | |
| knows_facts=("F1", "F2"), | |
| secrets=("Cornelius was about to cut you out of the club; his death secures everything.",), | |
| true_whereabouts=( | |
| WhereaboutsSegment(window=TimeWindow(start_min=m(21, 0), end_min=m(21, 20)), | |
| loc_id="L1", activity="greeting patrons in the lounge", | |
| co_present_sus_ids=("S2",)), | |
| WhereaboutsSegment(window=TimeWindow(start_min=m(21, 20), end_min=m(21, 50)), | |
| loc_id="L4", activity="confronting Cornelius in the office"), | |
| WhereaboutsSegment(window=TimeWindow(start_min=m(21, 50), end_min=m(22, 0)), | |
| loc_id="L5", activity="composing herself in the cloakroom"), | |
| ), | |
| stated_alibi=StatedAlibi( | |
| claim_text="I was in the lounge the entire evening, in plain sight of a dozen guests.", | |
| claimed_segments=(AlibiSegment(window=WINDOW, loc_id="L1", witness_sus_ids=("S2",)),), | |
| ), | |
| must_lie_about=("F1",), | |
| anchored_lies=( | |
| AnchoredLie( | |
| lie_id="LIE_alibi", topic="where you were during the murder", | |
| claimed="I never once left the lounge; ask any of the patrons.", | |
| truth_ref="F1", breaks_on=("C1", "C2"), | |
| fallback="Fine - I stepped out to the cloakroom for my wrap, that is all. I never " | |
| "went near the office.", | |
| ), | |
| ), | |
| visual=_visual("50s", "a black beaded evening gown", "imperious", "#b8860b", "female"), | |
| ) | |
| eddie = Suspect( | |
| sus_id="S2", name="Eddie Marsh", role="the bartender", | |
| persona_summary="Quick and affable, sweats easily, and keeps glancing at the till.", | |
| personality=PersonalityAxes(composure=0.3, aggression=0.2, evasiveness=0.6), | |
| tells=("wiping a bar that is already clean",), | |
| knows_facts=("F3",), | |
| secrets=("You have been skimming from the till for months.",), | |
| true_whereabouts=( | |
| WhereaboutsSegment(window=WINDOW, loc_id="L1", activity="tending the bar", | |
| co_present_sus_ids=("S1", "S4")), | |
| ), | |
| stated_alibi=StatedAlibi( | |
| claim_text="I never left the bar; somebody always wanted a drink.", | |
| claimed_segments=(AlibiSegment(window=WINDOW, loc_id="L1", witness_sus_ids=("S1", "S4")),), | |
| ), | |
| must_lie_about=("F3",), | |
| anchored_lies=( | |
| AnchoredLie(lie_id="LIE_till", topic="the till and the money", | |
| claimed="The till always comes up square; I would never touch it.", | |
| truth_ref="F3", breaks_on=("C3",), | |
| fallback="All right, I have borrowed against tips. It has nothing to do " | |
| "with Cornelius."), | |
| ), | |
| visual=_visual("30s", "shirtsleeves and a bar apron", "nervous", "#3a6ea5", "male"), | |
| ) | |
| lillian = Suspect( | |
| sus_id="S3", name="Lillian Frost", role="the headline singer", | |
| persona_summary="Theatrical and guarded about her private life, mourning beneath the glamour.", | |
| personality=PersonalityAxes(composure=0.6, aggression=0.4, evasiveness=0.5), | |
| tells=("a brittle laugh",), | |
| knows_facts=("F4",), | |
| secrets=("You were secretly in love with Cornelius and quarreled with him.",), | |
| true_whereabouts=( | |
| WhereaboutsSegment(window=WINDOW, loc_id="L3", activity="performing two sets on the terrace"), | |
| ), | |
| stated_alibi=StatedAlibi( | |
| claim_text="I was on the terrace, singing, the whole hour. Two hundred people saw me.", | |
| claimed_segments=(AlibiSegment(window=WINDOW, loc_id="L3"),), | |
| ), | |
| must_lie_about=("F4",), | |
| anchored_lies=( | |
| AnchoredLie(lie_id="LIE_affair", topic="your relationship with Cornelius", | |
| claimed="Cornelius and I were strictly professional.", | |
| truth_ref="F4", breaks_on=("C4",), | |
| fallback="We were close. I loved him. But I would never hurt him."), | |
| ), | |
| visual=_visual("20s", "a silver sequined stage dress", "guarded", "#9a9aa0", "female"), | |
| ) | |
| boone = Suspect( | |
| sus_id="S4", name="Septimus Boone", role="the club's accountant", | |
| persona_summary="Meticulous and nervous, speaks in numbers, and is terrified of an audit.", | |
| personality=PersonalityAxes(composure=0.4, aggression=0.2, evasiveness=0.7), | |
| tells=("polishing his spectacles",), | |
| knows_facts=("F5",), | |
| secrets=("You falsified the ledgers to hide your own embezzlement.",), | |
| true_whereabouts=( | |
| WhereaboutsSegment(window=WINDOW, loc_id="L1", activity="going over receipts at a corner table", | |
| co_present_sus_ids=("S2",)), | |
| ), | |
| stated_alibi=StatedAlibi( | |
| claim_text="I sat at my usual corner table in the lounge with my books all night.", | |
| claimed_segments=(AlibiSegment(window=WINDOW, loc_id="L1", witness_sus_ids=("S2",)),), | |
| ), | |
| must_lie_about=("F5",), | |
| anchored_lies=( | |
| AnchoredLie(lie_id="LIE_ledger", topic="the ledgers and the books", | |
| claimed="The books are in perfect order, I assure you.", | |
| truth_ref="F5", breaks_on=("C5",), | |
| fallback="There may be irregularities - mine, not murder. I was nowhere " | |
| "near him."), | |
| ), | |
| visual=_visual("40s", "a rumpled three-piece suit", "anxious", "#6b8f71", "male"), | |
| ) | |
| return (margot, eddie, lillian, boone) | |
| def build_tutorial_case() -> CaseFile: | |
| return CaseFile( | |
| case_id="tutorial-gilded-aerie", | |
| seed=1989, | |
| title="The Gilded Aerie", | |
| briefing=( | |
| "1924. On the rooftop of the Gilded Aerie supper club, owner Cornelius Vane is found " | |
| "dead in his office, struck down with his own brass award. The club was full; everyone " | |
| "had a reason to resent him. Four people could have slipped away during the murder hour. " | |
| "Question them, search the rooms, and present what you find. One of them is lying about " | |
| "where they were - catch that lie, and you have your killer." | |
| ), | |
| knobs=GenerationKnobs(setting_hint="rooftop supper club", era_hint="1924", | |
| tone_hint="jazz-age noir", n_suspects=4, n_red_herrings=3, | |
| difficulty=Difficulty.GENTLE), | |
| setting=Setting(name="The Gilded Aerie", description="A rooftop supper club above the city.", | |
| locations=_locations(), murder_window=WINDOW), | |
| victim=Victim(vic_id="V1", name="Cornelius Vane", role="club owner", found_at_loc_id="L4", | |
| found_at_min=m(22, 5), cause_of_death="blunt force from a brass statuette", | |
| time_of_death=TimeWindow(start_min=m(21, 20), end_min=m(21, 50))), | |
| weapon=Weapon(weapon_id="W1", name="brass award statuette", kind="blunt object", | |
| origin_loc_id="L4", requires_strength=False, leaves_trace="blood and a dented base"), | |
| suspects=_suspects(), | |
| culprit=Culprit( | |
| sus_id="S1", | |
| true_motive=Motive(motive_id="M1", category=MotiveCategory.GREED, | |
| summary="Cornelius was about to cut Margot out of the club; his death " | |
| "secures her inheritance."), | |
| method_narrative="You followed Cornelius to the office, argued about the will, and " | |
| "struck him with the brass statuette.", | |
| alibi_lie=AlibiLie(claimed_loc_id="L1", actual_loc_id="L4", | |
| contradicted_by_clue_ids=("C1", "C2")), | |
| ), | |
| relationships=( | |
| Relationship(from_sus_id="S1", to_sus_id="S2", kind="employer of", sentiment=0.1), | |
| Relationship(from_sus_id="S4", to_sus_id="S2", kind="friendly with", sentiment=0.3), | |
| ), | |
| facts=_facts(), | |
| clues=_clues(), | |
| solution=Solution( | |
| culprit_sus_id="S1", weapon_id="W1", motive_id="M1", minimal_clue_set=("C1",), | |
| deduction_chain=( | |
| "Cornelius was killed in the office between 9:20 and 9:50.", | |
| "Every suspect but Margot has a witness for that window.", | |
| "Margot swears she never left the lounge - yet her cloakroom ticket, stamped 9:48, " | |
| "was found beside the body.", | |
| "Her alibi is a lie; she was in the office. Margot Vane is the killer.", | |
| ), | |
| ), | |
| ) | |
| def main() -> None: | |
| case = build_tutorial_case() | |
| out = SEED_CASES_DIR / "tutorial.json" | |
| save_case(case, out) | |
| print(f"wrote {out} ({len(case.suspects)} suspects, {len(case.clues)} clues)") | |
| if __name__ == "__main__": | |
| main() | |