Spaces:
Sleeping
Sleeping
| """Component 1 β Case Generator. | |
| Generates randomized crime scenarios for the AI Crime Investigation World. | |
| Criminal is randomly assigned to either Suspect_A or Suspect_B each episode. | |
| """ | |
| import random | |
| from crime_env.constants import ( | |
| SUSPECTS, | |
| SUSPECT_A, | |
| SUSPECT_B, | |
| SUSPECT_A_KEY, | |
| SUSPECT_B_KEY, | |
| WITNESS_1, | |
| WITNESS_1_KEY, | |
| ) | |
| # ββ Pools for random sampling βββββββββββββββββββββββββββββββββββββββββββββββ | |
| CRIMES = [ | |
| "art theft", | |
| "corporate fraud", | |
| "warehouse robbery", | |
| "arson", | |
| ] | |
| LOCATIONS = [ | |
| "east wing", | |
| "server room", | |
| "vault", | |
| "parking lot", | |
| "basement archive", | |
| ] | |
| TIMES = [ | |
| "8:45 PM", | |
| "9:15 PM", | |
| "10:00 PM", | |
| ] | |
| METHODS = [ | |
| "lock picking with custom tools", | |
| "insider access using stolen credentials", | |
| "brute-force entry through side door", | |
| "social engineering the night guard", | |
| "disabling the alarm system remotely", | |
| "cloning a supervisor's keycard", | |
| "crawling through the ventilation shaft", | |
| "bribing a maintenance worker for access", | |
| "exploiting a gap in the security patrol rotation", | |
| "cutting the perimeter fence near the north wall", | |
| ] | |
| CLOTHING_OPTIONS = [ | |
| "dark hoodie and jeans", | |
| "grey trench coat", | |
| "black leather jacket", | |
| "navy blue overalls", | |
| "security guard uniform", | |
| "dark windbreaker and cargo pants", | |
| "green army surplus jacket", | |
| "plain black tracksuit", | |
| "brown corduroy jacket and khakis", | |
| "dark bomber jacket", | |
| ] | |
| MOTIVES = [ | |
| "financial pressure", | |
| "personal grudge", | |
| "professional rivalry", | |
| "jealousy", | |
| "revenge", | |
| "desperation", | |
| ] | |
| # ββ Type-constrained evidence templates βββββββββββββββββββββββββββββββββββββ | |
| # Each evidence type has its own pool to prevent mismatches. | |
| KEYCARD_LOG_TEMPLATES = [ | |
| { | |
| "description": "Keycard access log shows {suspect}'s badge was used at the {location} door at {time}", | |
| "points_to": "{suspect}", | |
| }, | |
| { | |
| "description": "Keycard system recorded an unauthorized swipe at {time} β badge ID unregistered", | |
| "points_to": "unknown", | |
| }, | |
| { | |
| "description": "Keycard records show two swipes at the {location} within 3 minutes of each other at {time} β one belonged to {suspect}", | |
| "points_to": "{suspect}", | |
| }, | |
| { | |
| "description": "Keycard system was offline for several hours due to a power glitch β no access logs available for the {location} until after {time}", | |
| "points_to": "unknown", | |
| }, | |
| ] | |
| CCTV_FOOTAGE_TEMPLATES = [ | |
| { | |
| "description": "CCTV footage shows a figure resembling {suspect} entering the {location} at {time}", | |
| "points_to": "{suspect}", | |
| }, | |
| { | |
| "description": "CCTV footage captured a person in dark clothing near the {location} at {time} β face obscured", | |
| "points_to": "unknown", | |
| }, | |
| { | |
| "description": "CCTV camera at {location} was disabled until shortly before {time} β no footage available for the relevant window", | |
| "points_to": "unknown", | |
| }, | |
| { | |
| "description": "CCTV from the adjacent hallway shows {suspect} walking briskly toward the {location} at {time}", | |
| "points_to": "{suspect}", | |
| }, | |
| ] | |
| FORENSIC_REPORT_TEMPLATES = [ | |
| { | |
| "description": "Forensic report: fingerprints matching {suspect} found on the tool bag recovered at the scene", | |
| "points_to": "{suspect}", | |
| }, | |
| { | |
| "description": "Forensic report: torn fabric consistent with a grey trench coat found near the {location}", | |
| "points_to": "unknown", | |
| }, | |
| { | |
| "description": "Forensic report: DNA trace on a dropped glove near the {location} matches {suspect}", | |
| "points_to": "{suspect}", | |
| }, | |
| { | |
| "description": "Forensic report: muddy boot prints (size 10) found at the scene β owner not yet identified", | |
| "points_to": "unknown", | |
| }, | |
| ] | |
| # ββ Specific alibi pools (no overlap between fake and real) βββββββββββββββββ | |
| FAKE_ALIBI_POOL = [ | |
| "I was at a family dinner at my uncle's house on Oak Street from 7 PM to 11 PM", | |
| "I was home alone watching a movie all evening β I didn't leave the house", | |
| "I was volunteering at the Riverside Community Centre until about 10:30 PM", | |
| "I was at my cousin's birthday party across town β there were at least 20 people there", | |
| "I was at the public library studying until they closed at 9 PM, then I walked home", | |
| "I was helping a friend move furniture into their new apartment on Elm Avenue", | |
| ] | |
| REAL_ALIBI_POOL = [ | |
| "I was working late at the Morgan & Associates office until 10 PM β my manager Sarah Chen can confirm, and I paid for takeout at 8:50 PM with my debit card", | |
| "I was at O'Brien's Bar on 5th Street from 7 PM to 11 PM β I paid my tab by card at 10:45 PM and the bartender knows me by name", | |
| "I was at the Cineplex on Main Street watching a 7:30 PM showing β I still have my ticket stub and the cashier can verify", | |
| "I was at the downtown gym from 6:30 PM to 9:30 PM β I scanned my membership card on entry and exit, which is logged electronically", | |
| "I was attending a night class at the community college from 7 PM to 9:30 PM β the instructor marked attendance electronically", | |
| "I was at St. Mary's Hospital visiting my grandmother from 6 PM to 9 PM β the visitor log has my name and sign-in time", | |
| ] | |
| BEHAVIORAL_PATTERNS = [ | |
| "uses family alibis", | |
| "deflects blame to associates", | |
| "becomes aggressive under pressure", | |
| "provides overly detailed timelines", | |
| "claims memory loss for key periods", | |
| "feigns cooperation while withholding details", | |
| ] | |
| PAST_CRIME_POOL = [ | |
| "petty theft", | |
| "fraud", | |
| "trespassing", | |
| "embezzlement", | |
| "forgery", | |
| "breaking and entering", | |
| ] | |
| LIE_TOPICS = ["location", "alibi", "knows_associate", "was_at_scene", "time"] | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _random_past_lies(count: int) -> list[dict]: | |
| """Generate a list of documented past lies.""" | |
| lies = [] | |
| for _ in range(count): | |
| topic = random.choice(LIE_TOPICS) | |
| lies.append( | |
| { | |
| "topic": topic, | |
| "claimed": _fake_claim(topic), | |
| "disproved_by": _disprove_source(topic), | |
| } | |
| ) | |
| return lies | |
| def _fake_claim(topic: str) -> str: | |
| claims = { | |
| "location": "claimed to be at home", | |
| "knows_associate": "denied knowing the accomplice", | |
| "was_at_scene": "denied being near the scene", | |
| "time": "claimed to be elsewhere during the incident window", | |
| "alibi": "said they were with a friend who later denied it", | |
| } | |
| return claims.get(topic, "made an unverifiable statement") | |
| def _disprove_source(topic: str) -> str: | |
| sources = { | |
| "location": "GPS records showed otherwise", | |
| "knows_associate": "phone records confirmed contact", | |
| "was_at_scene": "camera logs confirmed presence", | |
| "time": "timestamped records contradicted the timeline", | |
| "alibi": "friend's testimony contradicted the claim", | |
| } | |
| return sources.get(topic, "documentary evidence") | |
| def _build_evidence(criminal: str, location: str, time: str) -> list[dict]: | |
| """Build 3 pieces of physical evidence β one per type. | |
| Index 0 β keycard_log | |
| Index 1 β cctv_footage | |
| Index 2 β forensic_report | |
| Each type draws from its own template pool, ensuring descriptions | |
| always match the evidence type name. | |
| """ | |
| other_suspect = SUSPECT_B if criminal == SUSPECT_A else SUSPECT_A | |
| evidence_types = [ | |
| ("keycard_log", KEYCARD_LOG_TEMPLATES), | |
| ("cctv_footage", CCTV_FOOTAGE_TEMPLATES), | |
| ("forensic_report", FORENSIC_REPORT_TEMPLATES), | |
| ] | |
| evidence = [] | |
| for ev_name, pool in evidence_types: | |
| template = random.choice(pool) | |
| points_to_raw = template["points_to"] | |
| if points_to_raw == "{suspect}": | |
| # Bias toward implicating the actual criminal | |
| suspect = random.choices( | |
| [criminal, other_suspect], | |
| weights=[0.7, 0.3], | |
| )[0] | |
| else: | |
| suspect = None | |
| desc = template["description"].format( | |
| suspect=suspect or "", location=location, time=time | |
| ) | |
| points_to = suspect if suspect else "unknown" | |
| evidence.append( | |
| { | |
| "name": ev_name, | |
| "description": desc.strip(), | |
| "points_to": points_to, | |
| "visible_to_detective": False, | |
| } | |
| ) | |
| return evidence | |
| def _build_prior_history(criminal: str = SUSPECT_A) -> dict: | |
| """Build prior history for both suspects. | |
| The criminal is biased toward having a more checkered past | |
| (more prior crimes, lower trust), which improves realism. | |
| """ | |
| history = {} | |
| for suspect in SUSPECTS: | |
| is_criminal = suspect == criminal | |
| # Criminal: 1-2 priors, low trust; Innocent: 0-1, higher trust | |
| n_crimes = random.randint(1, 2) if is_criminal else random.randint(0, 1) | |
| n_lies = random.randint(1, 2) if is_criminal else random.randint(0, 1) | |
| on_parole = random.choices([True, False], weights=[0.8, 0.2])[0] if is_criminal else random.choice([True, False]) | |
| trust = round(random.uniform(0.0, 0.4), 2) if is_criminal else round(random.uniform(0.4, 1.0), 2) | |
| history[suspect] = { | |
| "previous_crimes": random.sample( | |
| PAST_CRIME_POOL, k=min(n_crimes, len(PAST_CRIME_POOL)) | |
| ), | |
| "past_lies_on_record": _random_past_lies(n_lies), | |
| "behavioral_pattern": random.choice(BEHAVIORAL_PATTERNS), | |
| "on_parole": on_parole, | |
| "parole_condition": ( | |
| random.choice( | |
| [ | |
| "must not leave the city", | |
| "mandatory weekly check-ins", | |
| "no contact with known criminals", | |
| "curfew between 9 PM and 6 AM", | |
| ] | |
| ) | |
| if on_parole | |
| else None | |
| ), | |
| "trust_score": trust, | |
| } | |
| return history | |
| def _build_agent_knowledge( | |
| criminal: str, location: str, time: str | |
| ) -> dict: | |
| """Build what each agent privately knows. | |
| Args: | |
| criminal: Which suspect is the criminal. | |
| location: Crime location. | |
| time: Crime time. | |
| """ | |
| innocent = SUSPECT_B if criminal == SUSPECT_A else SUSPECT_A | |
| fake_alibi = random.choice(FAKE_ALIBI_POOL) | |
| real_alibi = random.choice(REAL_ALIBI_POOL) | |
| clothing = random.choice(CLOTHING_OPTIONS) | |
| # Enhancement 2: 30% chance the witness got the clothing wrong | |
| witness_clothing = clothing | |
| if random.random() < 0.3: | |
| # Pick a different clothing item | |
| other_options = [c for c in CLOTHING_OPTIONS if c != clothing] | |
| witness_clothing = random.choice(other_options) | |
| knowledge = { | |
| SUSPECT_A_KEY if criminal == SUSPECT_A else SUSPECT_B_KEY: { | |
| "knows_own_guilt": True, | |
| "fake_alibi": fake_alibi, | |
| "knows_cctv_status": random.choice([True, False]), | |
| "knows_keycard_log_exists": random.choice([True, False]), | |
| }, | |
| SUSPECT_A_KEY if innocent == SUSPECT_A else SUSPECT_B_KEY: { | |
| "knows_own_guilt": False, | |
| "real_alibi": real_alibi, | |
| "knows_suspect_A": random.choice([True, False]), | |
| }, | |
| WITNESS_1_KEY: { | |
| "saw": f"a person in {witness_clothing} near the {location} around {time}", | |
| "actual_criminal_clothing": clothing, | |
| "time_seen": time, | |
| "bias_target": random.choice([SUSPECT_A, SUSPECT_B, None]), | |
| "bias_strength": round(random.uniform(0.0, 0.5), 2), | |
| }, | |
| } | |
| return knowledge | |
| def _build_detective_briefing(prior_history: dict) -> str: | |
| """Create a 2-3 sentence briefing for the detective (no criminal identity).""" | |
| parts = [] | |
| for suspect in SUSPECTS: | |
| h = prior_history[suspect] | |
| crimes = h["previous_crimes"] | |
| crime_str = ( | |
| f"has prior convictions for {', '.join(crimes)}" | |
| if crimes | |
| else "has no prior convictions on record" | |
| ) | |
| lies = h["past_lies_on_record"] | |
| lie_str = ( | |
| f"and has been caught lying about {lies[0]['topic']} in the past" | |
| if lies | |
| else "with no documented dishonesty" | |
| ) | |
| parole_str = "" | |
| if h["on_parole"]: | |
| parole_str = f" Currently on parole ({h['parole_condition']})." | |
| parts.append( | |
| f"{suspect} {crime_str} {lie_str}. " | |
| f"Known behavioral pattern: {h['behavioral_pattern']}.{parole_str}" | |
| ) | |
| return " ".join(parts) | |
| # ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_case() -> dict: | |
| """Generate a complete randomized crime case. | |
| Criminal is randomly assigned to either Suspect_A or Suspect_B. | |
| Returns a dictionary containing all case information needed for one | |
| episode of the Crime Investigation environment. | |
| """ | |
| crime = random.choice(CRIMES) | |
| criminal = random.choice(list(SUSPECTS)) | |
| time = random.choice(TIMES) | |
| location = random.choice(LOCATIONS) | |
| method = random.choice(METHODS) | |
| motive = random.choice(MOTIVES) | |
| physical_evidence = _build_evidence(criminal, location, time) | |
| prior_history = _build_prior_history(criminal) | |
| agent_knowledge = _build_agent_knowledge(criminal, location, time) | |
| detective_briefing = _build_detective_briefing(prior_history) | |
| return { | |
| "crime": crime, | |
| "criminal": criminal, | |
| "time": time, | |
| "location": location, | |
| "method": method, | |
| "motive": motive, | |
| "physical_evidence": physical_evidence, | |
| "prior_history": prior_history, | |
| "agent_knowledge": agent_knowledge, | |
| "detective_briefing": detective_briefing, | |
| } | |