Justxd22 commited on
Commit
13d61dc
·
1 Parent(s): 362fc3d

Implement core game mechanics and UI for murder mystery investigation

Browse files

- Added game engine with GameInstance class to manage game state, rounds, and suspect interactions.
- Integrated LLMManager for handling AI agents representing suspects and the detective.
- Developed scenario generator to load predefined crime scenarios based on difficulty.
- Created tools for investigation, including location tracking, footage retrieval, and DNA testing.
- Established a session management system to track active game instances.
- Designed prompts for different roles (detective, murderer, witness) to guide AI responses.
- Implemented a test suite to validate game logic and agent interactions.
- Developed basic UI components and styles for displaying evidence and suspect information.
- Added scenarios for various difficulty levels, including "The Gallery Heist Gone Wrong" and "The Coffee Shop Murder".

.gitignore CHANGED
@@ -1 +1,5 @@
1
- .mddd
 
 
 
 
 
1
+ .mddd
2
+ .env
3
+ __pycache__/
4
+ .vscode/
5
+ game/__pycache__/
GEMINI.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Murder.Ai Project Context
2
+
3
+ ## Project Overview
4
+ **Murder.Ai** is an AI-powered murder mystery game designed as a Gradio application. In this game, Large Language Models (LLMs) take on specific roles—Detective, Murderer, and Witnesses—to generate and solve unique crime scenarios.
5
+
6
+ The core concept involves an "MCP Story Generator" that creates consistent murder mystery cases. Users can watch the AI agents interact in "Spectator Mode" or actively participate in "Interactive Mode". The project is intended for deployment on Hugging Face Spaces.
7
+
8
+ ## Current Status
9
+ The project is currently in the **initialization phase**.
10
+ - **Detailed Plan:** A comprehensive design document exists in `PLan.md` outlining the game flow, data structures, and UI design.
11
+ - **Implementation:** The `app.py` file is currently a basic "Hello World" Gradio placeholder. The folder structure and core logic defined in the plan have not yet been implemented.
12
+
13
+ ## Architecture & Design
14
+ Based on `PLan.md`, the target architecture includes:
15
+
16
+ ### Tech Stack
17
+ - **Frontend:** Gradio 5.x with custom HTML/CSS/JS.
18
+ - **Backend:** Python 3.11+ with FastAPI (embedded in Gradio).
19
+ - **AI/LLM:** Integration with Anthropic Claude, OpenAI GPT-4, Google Gemini, and Meta Llama.
20
+ - **Tools:** Model Context Protocol (MCP) tools for in-game actions like `get_location`, `get_footage`, and `get_dna_test`.
21
+
22
+ ### Planned Structure
23
+ The roadmap suggests the following structure (to be implemented):
24
+ ```
25
+ murder-ai/
26
+ ├── app.py # Main Gradio app
27
+ ├── requirements.txt # Dependencies
28
+ ├── game/ # Game logic (scenario generator, engine, LLM manager)
29
+ ├── mcp/ # MCP tool definitions and server
30
+ ├── ui/ # Custom Gradio components and styles
31
+ ├── prompts/ # Role-specific system prompts
32
+ ├── scenarios/ # Pre-scripted fallback cases
33
+ └── assets/ # Images and media
34
+ ```
35
+
36
+ ## Development Conventions
37
+
38
+ ### Building & Running
39
+ Since the project is a Gradio app:
40
+ 1. **Install Dependencies:** (When `requirements.txt` is created)
41
+ ```bash
42
+ pip install -r requirements.txt
43
+ ```
44
+ 2. **Run the Application:**
45
+ ```bash
46
+ python app.py
47
+ ```
48
+ or
49
+ ```bash
50
+ gradio app.py
51
+ ```
52
+
53
+ ### Key Directives
54
+ - **Follow the Plan:** All development should align with the specifications in `PLan.md`.
55
+ - **Gradio 5:** Use the latest Gradio features, particularly for custom UI components and state management.
56
+ - **MCP Integration:** Tools should be designed to simulate real investigative data retrieval.
GRADIO_DOCS_llms.txt ADDED
The diff for this file is too large to render. See raw diff
 
PLan.md CHANGED
@@ -777,7 +777,7 @@ Deployment: HuggingFace Space
777
 
778
  ```yaml
779
  Frontend:
780
- - Gradio 5.x (required)
781
  - Custom HTML/CSS blocks for beautiful UI
782
  - JavaScript for animations
783
 
@@ -808,7 +808,7 @@ Deployment:
808
  - Modal for compute-heavy tasks (optional, for sponsor prize)
809
 
810
  Dependencies:
811
- - gradio>=5.0
812
  - anthropic
813
  - openai
814
  - google-generativeai
@@ -862,7 +862,7 @@ murder-ai/
862
 
863
  ## 🎨 **GRADIO UI ARCHITECTURE**
864
 
865
- ### **Modern Gradio 5 Approach:**
866
 
867
  ```python
868
  import gradio as gr
@@ -895,7 +895,7 @@ custom_css = """
895
 
896
  # Main Gradio Interface
897
  ```python
898
- with gr.Blocks(css=custom_css, theme=gr.themes.Noir()) as demo:
899
 
900
  gr.Markdown("# 🕵️ MURDER.AI")
901
 
@@ -947,5 +947,8 @@ with gr.Blocks(css=custom_css, theme=gr.themes.Noir()) as demo:
947
  outputs=[game_state, chatbot, tool_output, evidence_board]
948
  )
949
 
950
- demo.launch()
 
 
 
951
  ```
 
777
 
778
  ```yaml
779
  Frontend:
780
+ - Gradio 6.x (required)
781
  - Custom HTML/CSS blocks for beautiful UI
782
  - JavaScript for animations
783
 
 
808
  - Modal for compute-heavy tasks (optional, for sponsor prize)
809
 
810
  Dependencies:
811
+ - gradio>=6.0
812
  - anthropic
813
  - openai
814
  - google-generativeai
 
862
 
863
  ## 🎨 **GRADIO UI ARCHITECTURE**
864
 
865
+ ### **Modern Gradio 6 Approach:**
866
 
867
  ```python
868
  import gradio as gr
 
895
 
896
  # Main Gradio Interface
897
  ```python
898
+ with gr.Blocks() as demo:
899
 
900
  gr.Markdown("# 🕵️ MURDER.AI")
901
 
 
947
  outputs=[game_state, chatbot, tool_output, evidence_board]
948
  )
949
 
950
+ demo.launch(
951
+ theme=gr.themes.Noir(),
952
+ css=custom_css
953
+ )
954
  ```
README.md CHANGED
@@ -13,3 +13,4 @@ tags:
13
  - building-mcp-track-creative
14
  - mcp-in-action-track-creative
15
  ---
 
 
13
  - building-mcp-track-creative
14
  - mcp-in-action-track-creative
15
  ---
16
+ - Read `PLan.md`
app.py CHANGED
@@ -1,7 +1,228 @@
1
  import gradio as gr
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
+ import os
3
+ from game import game_engine
4
 
5
+ # Load CSS
6
+ with open("ui/styles.css", "r") as f:
7
+ custom_css = f.read()
8
 
9
+ def get_current_game(session_id):
10
+ if not session_id:
11
+ return None
12
+ return game_engine.get_game(session_id)
13
+
14
+ def format_suspect_card(suspect):
15
+ """Generates HTML for a suspect card."""
16
+ return f"""
17
+ <div class="suspect-card">
18
+ <h3>{suspect['name']}</h3>
19
+ <p><strong>Role:</strong> {suspect['role']}</p>
20
+ <p><strong>Bio:</strong> {suspect['bio']}</p>
21
+ <p>ID: {suspect['id']}</p>
22
+ </div>
23
+ """
24
+
25
+ def start_new_game_ui(difficulty):
26
+ """Starts a new game and returns initial UI state."""
27
+ session_id, game = game_engine.start_game(difficulty)
28
+
29
+ # Generate Suspect Cards HTML
30
+ cards = [format_suspect_card(s) for s in game.scenario["suspects"]]
31
+ # Pad if fewer than 4 (though scenarios have 4)
32
+ while len(cards) < 4:
33
+ cards.append("")
34
+
35
+ # Initial Evidence Board
36
+ evidence_html = f"<h3>Case: {game.scenario['title']}</h3><p>Victim: {game.scenario['victim']['name']}</p><hr>"
37
+
38
+ # Initial Chat
39
+ initial_chat = [
40
+ ("System", f"CASE FILE LOADED: {game.scenario['title']}"),
41
+ ("System", f"Victim: {game.scenario['victim']['name']}"),
42
+ ("System", f"Time of Death: {game.scenario['victim']['time_of_death']}")
43
+ ]
44
+
45
+ return (
46
+ session_id,
47
+ cards[0], cards[1], cards[2], cards[3], # Suspect cards
48
+ initial_chat, # Chatbot
49
+ evidence_html, # Evidence board
50
+ f"Round: {game.round}/5 | Points: {game.points}", # Stats
51
+ gr.update(choices=[(s['name'], s['id']) for s in game.scenario["suspects"]], value=None), # Suspect dropdown
52
+ gr.update(interactive=True), # Question input
53
+ gr.update(interactive=True), # Question btn
54
+ gr.update(interactive=True) # Tool btn
55
+ )
56
+
57
+ def submit_question(session_id, suspect_id, question, history):
58
+ game = get_current_game(session_id)
59
+ if not game:
60
+ return history, "Error: No active game."
61
+
62
+ if not suspect_id:
63
+ return history, "Error: Select a suspect first."
64
+
65
+ if not question:
66
+ return history, "Error: Enter a question."
67
+
68
+ response = game.question_suspect(suspect_id, question)
69
+
70
+ # Update chat
71
+ # Gradio chatbot format: list of [user_msg, bot_msg] or tuples.
72
+ # But here we have a multi-actor chat.
73
+ # Gradio's 'messages' type chatbot is better for this, but let's stick to standard
74
+ # tuple list (User, Bot) for simplicity, or just append to history.
75
+ # The plan said "Color-coded by speaker", implies standard Chatbot might be limiting
76
+ # if we want "Detective: ...", "Suspect: ...".
77
+ # We'll use (Speaker, Message) format and let Gradio handle it,
78
+ # or format it as "Speaker: Message" in the bubble.
79
+
80
+ history.append(("Detective", f"To {suspect_id}: {question}"))
81
+ history.append((suspect_id, response))
82
+
83
+ return history, "" # Clear input
84
+
85
+ def use_tool_ui(session_id, tool_name, arg1, arg2, history):
86
+ game = get_current_game(session_id)
87
+ if not game:
88
+ return history, "No game", "Error"
89
+
90
+ # Construct kwargs based on tool
91
+ kwargs = {}
92
+ if tool_name == "get_location":
93
+ kwargs = {"phone_number": arg1, "timestamp": arg2}
94
+ elif tool_name == "get_footage":
95
+ kwargs = {"location": arg1, "time_range": arg2}
96
+ elif tool_name == "get_dna_test":
97
+ kwargs = {"evidence_id": arg1}
98
+ elif tool_name == "call_alibi":
99
+ kwargs = {"phone_number": arg1}
100
+
101
+ result = game.use_tool(tool_name, **kwargs)
102
+
103
+ # Update History
104
+ history.append(("System", f"Used {tool_name}: {result}"))
105
+
106
+ # Update Evidence Board
107
+ ev_html = f"<h3>Case: {game.scenario['title']}</h3><p>Victim: {game.scenario['victim']['name']}</p><hr><h4>Evidence Revealed:</h4><ul>"
108
+ for item in game.evidence_revealed:
109
+ ev_html += f"<li>{str(item)}</li>"
110
+ ev_html += "</ul>"
111
+
112
+ stats = f"Round: {game.round}/5 | Points: {game.points}"
113
+
114
+ return history, ev_html, stats
115
+
116
+ def next_round_ui(session_id):
117
+ game = get_current_game(session_id)
118
+ if not game:
119
+ return "No game", "Error"
120
+
121
+ not_over = game.advance_round()
122
+ stats = f"Round: {game.round}/5 | Points: {game.points}"
123
+
124
+ if not not_over:
125
+ stats += " [GAME OVER]"
126
+
127
+ return stats
128
+
129
+
130
+ # --- UI LAYOUT ---
131
+
132
+ with gr.Blocks(title="Murder.Ai") as demo:
133
+
134
+ gr.Markdown("# 🕵️ MURDER.AI")
135
+
136
+ session_state = gr.State("")
137
+
138
+ with gr.Row():
139
+ # Left: Suspect Cards
140
+ with gr.Column(scale=1):
141
+ gr.Markdown("## Suspects")
142
+ suspect_1 = gr.HTML(elem_classes="suspect-card")
143
+ suspect_2 = gr.HTML(elem_classes="suspect-card")
144
+ suspect_3 = gr.HTML(elem_classes="suspect-card")
145
+ suspect_4 = gr.HTML(elem_classes="suspect-card")
146
+
147
+ # Center: Main Game Area
148
+ with gr.Column(scale=2):
149
+ game_stats = gr.Markdown("Round: 0/5 | Points: 10")
150
+
151
+ chatbot = gr.Chatbot(
152
+ label="Investigation Log",
153
+ height=500,
154
+ type="messages" # Gradio 5/6 new format
155
+ )
156
+
157
+ with gr.Row():
158
+ suspect_dropdown = gr.Dropdown(label="Select Suspect", choices=[])
159
+ question_input = gr.Textbox(label="Question", placeholder="Where were you?", interactive=False)
160
+ ask_btn = gr.Button("Ask", variant="secondary", interactive=False)
161
+
162
+ gr.Markdown("### Tools")
163
+ with gr.Row():
164
+ tool_dropdown = gr.Dropdown(
165
+ label="Select Tool",
166
+ choices=["get_location", "get_footage", "get_dna_test", "call_alibi"],
167
+ value="get_location"
168
+ )
169
+ arg1_input = gr.Textbox(label="Arg 1 (Phone/Loc/ID)")
170
+ arg2_input = gr.Textbox(label="Arg 2 (Time)")
171
+ use_tool_btn = gr.Button("Use Tool", interactive=False)
172
+
173
+ next_round_btn = gr.Button("▶️ Next Round / End Game")
174
+
175
+ # Right: Evidence Board
176
+ with gr.Column(scale=1):
177
+ gr.Markdown("## Evidence Board")
178
+ evidence_board = gr.HTML(
179
+ elem_classes="evidence-board",
180
+ value="<p>No active case.</p>"
181
+ )
182
+
183
+ # Start Controls
184
+ with gr.Row():
185
+ difficulty_selector = gr.Radio(["Easy", "Medium", "Hard"], label="Difficulty", value="Medium")
186
+ start_btn = gr.Button("🎲 GENERATE NEW CASE", variant="primary")
187
+
188
+ # --- EVENTS ---
189
+
190
+ start_btn.click(
191
+ fn=start_new_game_ui,
192
+ inputs=[difficulty_selector],
193
+ outputs=[
194
+ session_state,
195
+ suspect_1, suspect_2, suspect_3, suspect_4,
196
+ chatbot,
197
+ evidence_board,
198
+ game_stats,
199
+ suspect_dropdown,
200
+ question_input,
201
+ ask_btn,
202
+ use_tool_btn
203
+ ]
204
+ )
205
+
206
+ ask_btn.click(
207
+ fn=submit_question,
208
+ inputs=[session_state, suspect_dropdown, question_input, chatbot],
209
+ outputs=[chatbot, question_input]
210
+ )
211
+
212
+ use_tool_btn.click(
213
+ fn=use_tool_ui,
214
+ inputs=[session_state, tool_dropdown, arg1_input, arg2_input, chatbot],
215
+ outputs=[chatbot, evidence_board, game_stats]
216
+ )
217
+
218
+ next_round_btn.click(
219
+ fn=next_round_ui,
220
+ inputs=[session_state],
221
+ outputs=[game_stats]
222
+ )
223
+
224
+ demo.launch(
225
+ theme=gr.themes.Noir(),
226
+ css=custom_css,
227
+ allowed_paths=["."]
228
+ )
game/__init__.py ADDED
File without changes
game/game_engine.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from .scenario_generator import generate_crime_scenario
3
+ from .llm_manager import LLMManager
4
+ from mcp import tools
5
+
6
+ class GameInstance:
7
+ def __init__(self, difficulty="medium"):
8
+ self.id = str(uuid.uuid4())
9
+ self.scenario = generate_crime_scenario(difficulty)
10
+ self.llm_manager = LLMManager()
11
+ self.round = 1
12
+ self.max_rounds = 5
13
+ self.points = 10
14
+ self.evidence_revealed = [] # List of strings or result dicts
15
+ self.logs = [] # Chat logs
16
+ self.game_over = False
17
+ self.verdict_correct = False
18
+
19
+ # Initialize Agents
20
+ self._init_agents()
21
+
22
+ def _init_agents(self):
23
+ # 1. Detective
24
+ detective_context = {
25
+ "victim_name": self.scenario["victim"]["name"],
26
+ "time_of_death": self.scenario["victim"]["time_of_death"],
27
+ "location": self.scenario["evidence"]["location_data"].get("suspect_1_phone", {}).get("8:47 PM", {}).get("location", "Unknown"), # Approximate
28
+ "investigation_state": "Initial briefing."
29
+ }
30
+ self.llm_manager.create_agent("detective", "detective", detective_context)
31
+
32
+ # 2. Suspects
33
+ for suspect in self.scenario["suspects"]:
34
+ role = "murderer" if suspect["is_murderer"] else "witness"
35
+
36
+ # Context for prompt
37
+ context = {
38
+ "name": suspect["name"],
39
+ "victim_name": self.scenario["victim"]["name"],
40
+ "alibi_story": suspect["alibi_story"],
41
+ "bio": suspect["bio"],
42
+ "true_location": suspect["true_location"],
43
+
44
+ # Murderer specific
45
+ "method": self.scenario["title"], # Placeholder, scenario doesn't explicitly list 'method' field in plan, using title/weapon logic implied
46
+ "location": self.scenario["victim"].get("location", "the scene"), # Using generic
47
+ "motive": suspect["motive"]
48
+ }
49
+
50
+ self.llm_manager.create_agent(suspect["id"], role, context)
51
+
52
+ def log_event(self, speaker, message):
53
+ self.logs.append({"speaker": speaker, "message": message})
54
+
55
+ def question_suspect(self, suspect_id, question):
56
+ if self.game_over:
57
+ return "Game Over"
58
+
59
+ suspect_name = next((s["name"] for s in self.scenario["suspects"] if s["id"] == suspect_id), "Unknown")
60
+
61
+ # 1. Detective asks (simulated log)
62
+ self.log_event("Detective", f"To {suspect_name}: {question}")
63
+
64
+ # 2. Suspect responds
65
+ response = self.llm_manager.get_response(suspect_id, question)
66
+ self.log_event(suspect_name, response)
67
+
68
+ return response
69
+
70
+ def use_tool(self, tool_name, **kwargs):
71
+ if self.points <= 0:
72
+ return {"error": "Not enough investigation points!"}
73
+
74
+ cost = 0
75
+ result = {}
76
+
77
+ # Map tool names to functions
78
+ if tool_name == "get_location":
79
+ cost = 2
80
+ result = tools.get_location(self.scenario, kwargs.get("phone_number"), kwargs.get("timestamp"))
81
+ elif tool_name == "get_footage":
82
+ cost = 2
83
+ result = tools.get_footage(self.scenario, kwargs.get("location"), kwargs.get("time_range"))
84
+ elif tool_name == "get_dna_test":
85
+ cost = 3
86
+ result = tools.get_dna_test(self.scenario, kwargs.get("evidence_id"))
87
+ elif tool_name == "call_alibi":
88
+ cost = 1
89
+ result = tools.call_alibi(self.scenario, kwargs.get("phone_number"))
90
+ else:
91
+ return {"error": f"Unknown tool: {tool_name}"}
92
+
93
+ if "error" in result:
94
+ return result # Don't deduct points for errors
95
+
96
+ self.points -= cost
97
+ self.evidence_revealed.append(result)
98
+ self.log_event("System", f"Used {tool_name}. Cost: {cost} pts. Result: {str(result)}")
99
+ return result
100
+
101
+ def advance_round(self):
102
+ if self.round < self.max_rounds:
103
+ self.round += 1
104
+ return True
105
+ else:
106
+ self.game_over = True
107
+ return False
108
+
109
+ def make_accusation(self, suspect_id):
110
+ self.game_over = True
111
+
112
+ # Check if correct
113
+ murderer = next((s for s in self.scenario["suspects"] if s["is_murderer"]), None)
114
+
115
+ if murderer and murderer["id"] == suspect_id:
116
+ self.verdict_correct = True
117
+ msg = f"CORRECT! {murderer['name']} was the murderer."
118
+ else:
119
+ self.verdict_correct = False
120
+ msg = f"INCORRECT. The true murderer was {murderer['name'] if murderer else 'Unknown'}."
121
+
122
+ self.log_event("System", f"FINAL ACCUSATION: {suspect_id}. Result: {msg}")
123
+ return msg
124
+
125
+ # Global Session Store
126
+ SESSIONS = {}
127
+
128
+ def start_game(difficulty="medium"):
129
+ game = GameInstance(difficulty)
130
+ SESSIONS[game.id] = game
131
+ return game.id, game
132
+
133
+ def get_game(session_id):
134
+ return SESSIONS.get(session_id)
game/llm_manager.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import google.generativeai as genai
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ # Configure the API
8
+ API_KEY = os.getenv("GEMINI_API_KEY")
9
+ if API_KEY:
10
+ genai.configure(api_key=API_KEY)
11
+
12
+ class GeminiAgent:
13
+ def __init__(self, model_name="gemini-2.5-flash", system_instruction=None):
14
+ self.model_name = model_name
15
+ self.system_instruction = system_instruction
16
+ self.chat_session = None
17
+ self.history = []
18
+
19
+ if API_KEY:
20
+ self.model = genai.GenerativeModel(
21
+ model_name=model_name,
22
+ system_instruction=system_instruction
23
+ )
24
+ self.chat_session = self.model.start_chat(history=[])
25
+ else:
26
+ print("Warning: No GEMINI_API_KEY found. Agent will run in mock mode.")
27
+ self.model = None
28
+
29
+ def generate_response(self, user_input):
30
+ if not self.model:
31
+ return f"[MOCK] I received: {user_input}. (Set GEMINI_API_KEY to get real responses)"
32
+
33
+ try:
34
+ response = self.chat_session.send_message(user_input)
35
+ return response.text
36
+ except Exception as e:
37
+ return f"Error generating response: {str(e)}"
38
+
39
+ class LLMManager:
40
+ def __init__(self):
41
+ self.agents = {}
42
+ self.prompts = self._load_prompts()
43
+
44
+ def _load_prompts(self):
45
+ prompts = {}
46
+ prompt_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "prompts")
47
+ for filename in ["murderer.txt", "witness.txt", "detective.txt"]:
48
+ key = filename.replace(".txt", "")
49
+ try:
50
+ with open(os.path.join(prompt_dir, filename), "r") as f:
51
+ prompts[key] = f.read()
52
+ except FileNotFoundError:
53
+ print(f"Warning: Prompt file {filename} not found.")
54
+ prompts[key] = ""
55
+ return prompts
56
+
57
+ def create_agent(self, agent_id, role, context_data):
58
+ """
59
+ Creates a new GeminiAgent for a specific character.
60
+
61
+ agent_id: Unique ID (e.g., 'suspect_1', 'detective')
62
+ role: 'murderer', 'witness', 'detective'
63
+ context_data: Dict to fill in the prompt templates (name, victim_name, etc.)
64
+ """
65
+
66
+ # Select base prompt template
67
+ if role == "murderer":
68
+ base_prompt = self.prompts.get("murderer", "")
69
+ elif role == "detective":
70
+ base_prompt = self.prompts.get("detective", "")
71
+ else:
72
+ base_prompt = self.prompts.get("witness", "")
73
+
74
+ # Fill template
75
+ try:
76
+ system_instruction = base_prompt.format(**context_data)
77
+ except KeyError as e:
78
+ print(f"Warning: Missing key {e} in context data for {agent_id}")
79
+ system_instruction = base_prompt # Fallback
80
+
81
+ # Create agent
82
+ agent = GeminiAgent(system_instruction=system_instruction)
83
+ self.agents[agent_id] = agent
84
+ return agent
85
+
86
+ def get_agent(self, agent_id):
87
+ return self.agents.get(agent_id)
88
+
89
+ def get_response(self, agent_id, user_input):
90
+ agent = self.get_agent(agent_id)
91
+ if agent:
92
+ return agent.generate_response(user_input)
93
+ return "Error: Agent not found."
game/scenario_generator.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import random
3
+ import os
4
+
5
+ # Path to scenarios directory
6
+ SCENARIOS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "scenarios")
7
+
8
+ def load_scenario(filename):
9
+ """Loads a scenario from a JSON file."""
10
+ path = os.path.join(SCENARIOS_DIR, filename)
11
+ try:
12
+ with open(path, "r") as f:
13
+ return json.load(f)
14
+ except FileNotFoundError:
15
+ print(f"Error: Scenario file {filename} not found at {path}")
16
+ return None
17
+ except json.JSONDecodeError:
18
+ print(f"Error: Invalid JSON in {filename}")
19
+ return None
20
+
21
+ def generate_crime_scenario(difficulty="medium"):
22
+ """
23
+ Currently picks a pre-scripted scenario based on difficulty or random.
24
+ In future, this will call an LLM to generate unique JSON.
25
+ """
26
+
27
+ # For prototype, map difficulty to specific files or random
28
+ # We have: silicon_valley.json (Medium), coffee_shop.json (Easy), art_gallery.json (Hard)
29
+
30
+ if difficulty.lower() == "easy":
31
+ chosen_file = "coffee_shop.json"
32
+ elif difficulty.lower() == "hard":
33
+ chosen_file = "art_gallery.json"
34
+ else:
35
+ chosen_file = "silicon_valley.json"
36
+
37
+ # Or just pick random for variety if difficulty not strictly enforced
38
+ # chosen_file = random.choice(["silicon_valley.json", "coffee_shop.json", "art_gallery.json"])
39
+
40
+ scenario = load_scenario(chosen_file)
41
+
42
+ if not scenario:
43
+ # Fallback
44
+ return load_scenario("silicon_valley.json")
45
+
46
+ return scenario
mcp/__init__.py ADDED
File without changes
mcp/server.py ADDED
File without changes
mcp/tools.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+
3
+ def find_suspect_by_phone(case_data, phone_number):
4
+ """Helper to find a suspect ID by their phone number."""
5
+ for suspect in case_data["suspects"]:
6
+ if suspect["phone_number"] == phone_number:
7
+ return suspect["id"]
8
+ return None
9
+
10
+ def get_suspect_name(case_data, suspect_id):
11
+ """Helper to get a suspect's name by ID."""
12
+ for suspect in case_data["suspects"]:
13
+ if suspect["id"] == suspect_id:
14
+ return suspect["name"]
15
+ return "Unknown"
16
+
17
+ def get_location(case_data, phone_number: str, timestamp: str) -> dict:
18
+ """Query the case database for location data."""
19
+
20
+ # Find which suspect has this phone number
21
+ suspect_id = find_suspect_by_phone(case_data, phone_number)
22
+
23
+ if not suspect_id:
24
+ return {"error": "Phone number not associated with any suspect."}
25
+
26
+ # Look up their location at this time
27
+ # Structure: case_data["evidence"]["location_data"][f"{suspect_id}_phone"][timestamp]
28
+ phone_key = f"{suspect_id}_phone"
29
+ location_data = case_data.get("evidence", {}).get("location_data", {}).get(phone_key, {}).get(timestamp)
30
+
31
+ if not location_data:
32
+ return {"error": f"No location data found for {phone_number} at {timestamp}."}
33
+
34
+ return {
35
+ "timestamp": timestamp,
36
+ "coordinates": f"{location_data['lat']}, {location_data['lng']}",
37
+ "description": location_data['location'],
38
+ "accuracy": "Cell tower triangulation ±50m"
39
+ }
40
+
41
+ def get_footage(case_data, location: str, time_range: str) -> dict:
42
+ """Query case database for camera footage."""
43
+
44
+ footage = case_data.get("evidence", {}).get("footage_data", {}).get(location, {}).get(time_range)
45
+
46
+ if not footage:
47
+ return {"error": "No camera footage available at this location/time."}
48
+
49
+ return {
50
+ "location": location,
51
+ "time_range": time_range,
52
+ "visible_people": footage["visible_people"],
53
+ "quality": footage["quality"],
54
+ "key_details": footage.get("key_frame", "No significant events")
55
+ }
56
+
57
+ def get_dna_test(case_data, evidence_id: str) -> dict:
58
+ """Query case database for DNA/fingerprint evidence."""
59
+
60
+ dna = case_data.get("evidence", {}).get("dna_evidence", {}).get(evidence_id)
61
+
62
+ if not dna:
63
+ return {"error": "Evidence not found or not testable."}
64
+
65
+ # Simulate processing time
66
+ # time.sleep(1)
67
+
68
+ primary_match_name = get_suspect_name(case_data, dna.get("primary_match"))
69
+
70
+ return {
71
+ "evidence_id": evidence_id,
72
+ "primary_match": primary_match_name,
73
+ "confidence": dna["confidence"],
74
+ "notes": dna["notes"]
75
+ }
76
+
77
+ def call_alibi(case_data, phone_number: str) -> dict:
78
+ """Call an alibi witness."""
79
+
80
+ # Find alibi in database
81
+ alibi = None
82
+ for suspect_alibi in case_data.get("evidence", {}).get("alibis", {}).values():
83
+ if suspect_alibi.get("contact") == phone_number:
84
+ alibi = suspect_alibi
85
+ break
86
+
87
+ if not alibi:
88
+ return {"error": "Number not found."}
89
+
90
+ # If alibi is truthful, confirm story
91
+ if alibi["truth"].startswith("Telling truth"):
92
+ response = f"Yes, I can confirm they were with me at that time."
93
+ else:
94
+ # Lying/Uncertain
95
+ response = f"Uh... yes, they were with me. Definitely."
96
+
97
+ return {
98
+ "contact_name": alibi.get("contact_name", "Unknown"),
99
+ "response": response,
100
+ "confidence": "High" if alibi["verifiable"] else "Uncertain",
101
+ "red_flags": [] if alibi["verifiable"] else ["Hesitant response", "No details provided"]
102
+ }
prompts/alibi.txt ADDED
File without changes
prompts/detective.txt ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are the lead detective investigating a murder.
2
+
3
+ CASE: {victim_name}, found dead at {time_of_death}
4
+ LOCATION: {location}
5
+
6
+ YOUR OBJECTIVE:
7
+ - Question the suspects
8
+ - Use your tools strategically
9
+ - Identify the murderer with EVIDENCE, not just intuition
10
+
11
+ STRATEGY TIPS:
12
+ - Start broad, then focus on inconsistencies
13
+ - Look for story changes
14
+ - Cross-reference evidence
15
+ - Build a logical case before accusing
16
+
17
+ PERSONALITY:
18
+ - Methodical and logical
19
+ - Reference evidence in questioning
20
+ - Show your reasoning process
21
+ - Confident but not arrogant
22
+
23
+ Current state of investigation:
24
+ {investigation_state}
prompts/murderer.txt ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CONFIDENTIAL - EYES ONLY
2
+
3
+ You are {name}, a suspect in a murder investigation.
4
+
5
+ TRUTH: You ARE the murderer.
6
+
7
+ CASE DETAILS:
8
+ - Victim: {victim_name}
9
+ - Method: {method}
10
+ - Location: {location}
11
+ - Motive: {motive}
12
+
13
+ YOUR COVER STORY:
14
+ - You were "{alibi_story}"
15
+ - You have no alibi witnesses (unless specified otherwise)
16
+
17
+ YOUR OBJECTIVE:
18
+ - Convince the detective you're innocent
19
+ - Lie convincingly but not obviously
20
+ - When caught in lies, have backup explanations
21
+ - Show appropriate emotional responses (nervous but not guilty)
22
+ - If evidence becomes overwhelming, consider partial confession (but not murder)
23
+
24
+ PERSONALITY:
25
+ - {bio}
26
+ - Don't break character - you MUST try to get away with it
27
+
28
+ TOOLS YOU CANNOT USE:
29
+ - You cannot access MCP tools
30
+ - You can only respond to questions
31
+
32
+ Remember: You're trying to WIN. Don't make it easy for detective.
prompts/witness.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are {name}, a suspect in a murder investigation.
2
+
3
+ TRUTH: You are INNOCENT. You did not commit the crime.
4
+
5
+ CASE DETAILS:
6
+ - Victim: {victim_name}
7
+
8
+ YOUR ALIBI:
9
+ - You were "{alibi_story}"
10
+ - You were at {true_location}
11
+
12
+ YOUR OBJECTIVE:
13
+ - Tell the truth about where you were
14
+ - You might be nervous or defensive if you have secrets (like {bio})
15
+ - Cooperate if treated well, clam up if accused falsely
16
+
17
+ PERSONALITY:
18
+ - {bio}
19
+ - Don't break character.
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=5.0
2
+ google-generativeai
3
+ python-dotenv
scenarios/art_gallery.json ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "case_id": "C99",
3
+ "title": "The Gallery Heist Gone Wrong",
4
+ "difficulty": "Hard",
5
+ "victim": {
6
+ "name": "Vincent Shaw",
7
+ "age": 55,
8
+ "occupation": "Art Dealer",
9
+ "time_of_death": "10:00 PM"
10
+ },
11
+ "suspects": [
12
+ {
13
+ "id": "suspect_1",
14
+ "name": "Julian Thorne",
15
+ "role": "Assistant Curator",
16
+ "is_murderer": true,
17
+ "motive": "Theft of 'The Red Sunset'",
18
+ "true_location": "Gallery Wine Cellar",
19
+ "alibi_story": "Stocktaking in basement",
20
+ "phone_number": "+1-555-2101",
21
+ "bio": "Sophisticated but in debt. Has been eyeing the collection."
22
+ },
23
+ {
24
+ "id": "suspect_2",
25
+ "name": "Elena Rossi",
26
+ "role": "Artist",
27
+ "is_murderer": false,
28
+ "motive": "Contract dispute",
29
+ "true_location": "Main Hall",
30
+ "alibi_story": "Talking to guests",
31
+ "phone_number": "+1-555-2102",
32
+ "bio": "Passionate and volatile. Felt Vincent was cheating her."
33
+ },
34
+ {
35
+ "id": "suspect_3",
36
+ "name": "Arthur Pym",
37
+ "role": "Collector",
38
+ "is_murderer": false,
39
+ "motive": "Blackmail",
40
+ "true_location": "Sculpture Garden",
41
+ "alibi_story": "Smoking cigar",
42
+ "phone_number": "+1-555-2103",
43
+ "bio": "Wealthy and secretive. Had a secret history with Vincent."
44
+ },
45
+ {
46
+ "id": "suspect_4",
47
+ "name": "Maria Gonzales",
48
+ "role": "Caterer",
49
+ "is_murderer": false,
50
+ "motive": "None",
51
+ "true_location": "Kitchen",
52
+ "alibi_story": "Preparing hors d'oeuvres",
53
+ "phone_number": "+1-555-2104",
54
+ "bio": "Hardworking. Just there to do a job."
55
+ }
56
+ ],
57
+ "evidence": {
58
+ "location_data": {
59
+ "suspect_1_phone": {
60
+ "10:00 PM": {"lat": 51.5074, "lng": -0.1278, "location": "Gallery Office"}
61
+ },
62
+ "suspect_2_phone": {
63
+ "10:00 PM": {"lat": 51.5074, "lng": -0.1279, "location": "Gallery Main Hall"}
64
+ }
65
+ },
66
+ "footage_data": {
67
+ "office_cam": {
68
+ "9:50-10:10 PM": {
69
+ "visible_people": ["Vincent drinking wine", "Julian pouring more wine"],
70
+ "quality": "High",
71
+ "key_frame": "9:55 PM - Julian adds powder to glass"
72
+ }
73
+ }
74
+ },
75
+ "dna_evidence": {
76
+ "wine_glass": {
77
+ "primary_match": "suspect_1",
78
+ "confidence": "80%",
79
+ "notes": "Fingerprints of Julian on the stem"
80
+ },
81
+ "poison_vial": {
82
+ "primary_match": "suspect_1",
83
+ "confidence": "99%",
84
+ "notes": "Found in Julian's pocket"
85
+ }
86
+ },
87
+ "alibis": {
88
+ "suspect_1_alibi": {
89
+ "contact": "+1-555-2200",
90
+ "contact_name": "Maria (Caterer)",
91
+ "verifiable": true,
92
+ "truth": "Lying - Maria didn't see him"
93
+ }
94
+ }
95
+ },
96
+ "timeline": {
97
+ "9:55 PM": "Julian poisons wine",
98
+ "10:00 PM": "Vincent collapses",
99
+ "10:05 PM": "Julian calls 911 feigning panic"
100
+ }
101
+ }
scenarios/coffee_shop.json ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "case_id": "B12",
3
+ "title": "The Coffee Shop Murder",
4
+ "difficulty": "Easy",
5
+ "victim": {
6
+ "name": "Emma Rodriguez",
7
+ "age": 24,
8
+ "occupation": "Barista",
9
+ "time_of_death": "11:15 PM"
10
+ },
11
+ "suspects": [
12
+ {
13
+ "id": "suspect_1",
14
+ "name": "Liam O'Connor",
15
+ "role": "Ex-Boyfriend",
16
+ "is_murderer": true,
17
+ "motive": "Jealousy",
18
+ "true_location": "Coffee Shop Back Door",
19
+ "alibi_story": "Gaming at home",
20
+ "phone_number": "+1-555-1101",
21
+ "bio": "Possessive and recently dumped. Has been sending angry texts."
22
+ },
23
+ {
24
+ "id": "suspect_2",
25
+ "name": "Mark Stevens",
26
+ "role": "Manager",
27
+ "is_murderer": false,
28
+ "motive": "Financial dispute",
29
+ "true_location": "Bank deposit drop",
30
+ "alibi_story": "Depositing cash",
31
+ "phone_number": "+1-555-1102",
32
+ "bio": "Stressed about money. Owed Emma back pay."
33
+ },
34
+ {
35
+ "id": "suspect_3",
36
+ "name": "Chloe Kim",
37
+ "role": "Coworker",
38
+ "is_murderer": false,
39
+ "motive": "Promotion rivalry",
40
+ "true_location": "Gym",
41
+ "alibi_story": "Late night workout",
42
+ "phone_number": "+1-555-1103",
43
+ "bio": "Competitive and ambitious. Wanted the shift lead position."
44
+ },
45
+ {
46
+ "id": "suspect_4",
47
+ "name": "Regular Joe",
48
+ "role": "Customer",
49
+ "is_murderer": false,
50
+ "motive": "Obsession",
51
+ "true_location": "Bus stop across street",
52
+ "alibi_story": "Waiting for bus",
53
+ "phone_number": "+1-555-1104",
54
+ "bio": "Always there. Kind of creepy but harmless."
55
+ }
56
+ ],
57
+ "evidence": {
58
+ "location_data": {
59
+ "suspect_1_phone": {
60
+ "11:15 PM": {"lat": 40.7128, "lng": -74.0060, "location": "Coffee Shop Alley"}
61
+ },
62
+ "suspect_2_phone": {
63
+ "11:15 PM": {"lat": 40.7150, "lng": -74.0080, "location": "Night Deposit Box"}
64
+ },
65
+ "suspect_3_phone": {
66
+ "11:15 PM": {"lat": 40.7200, "lng": -74.0100, "location": "24h Gym"}
67
+ },
68
+ "suspect_4_phone": {
69
+ "11:15 PM": {"lat": 40.7129, "lng": -74.0059, "location": "Bus Stop"}
70
+ }
71
+ },
72
+ "footage_data": {
73
+ "kitchen_cam": {
74
+ "11:10-11:20 PM": {
75
+ "visible_people": ["Emma arguing with man in blue cap"],
76
+ "quality": "Good",
77
+ "key_frame": "11:15 PM - Man stabs Emma"
78
+ }
79
+ },
80
+ "alley_cam": {
81
+ "11:00-11:30 PM": {
82
+ "visible_people": ["Liam entering 11:05", "Liam running out 11:16"],
83
+ "notes": "Wearing blue cap"
84
+ }
85
+ }
86
+ },
87
+ "dna_evidence": {
88
+ "knife_handle": {
89
+ "primary_match": "suspect_1",
90
+ "confidence": "99%",
91
+ "notes": "Sweat and skin cells found"
92
+ }
93
+ },
94
+ "alibis": {
95
+ "suspect_1_alibi": {
96
+ "contact": "None",
97
+ "verifiable": false,
98
+ "truth": "Lying - claims gaming alone"
99
+ },
100
+ "suspect_2_alibi": {
101
+ "contact": "+1-555-1200",
102
+ "contact_name": "Bank CCTV",
103
+ "verifiable": true,
104
+ "truth": "Telling truth - footage confirms"
105
+ }
106
+ }
107
+ },
108
+ "timeline": {
109
+ "11:00 PM": "Shop closes",
110
+ "11:05 PM": "Liam enters via back door",
111
+ "11:15 PM": "Murder occurs",
112
+ "11:16 PM": "Liam flees"
113
+ }
114
+ }
scenarios/silicon_valley.json ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "case_id": "A47",
3
+ "title": "The Silicon Valley Incident",
4
+ "difficulty": "Medium",
5
+ "victim": {
6
+ "name": "Marcus Chen",
7
+ "age": 42,
8
+ "occupation": "Tech CEO",
9
+ "time_of_death": "8:47 PM"
10
+ },
11
+ "suspects": [
12
+ {
13
+ "id": "suspect_1",
14
+ "name": "Sarah Johnson",
15
+ "role": "CFO",
16
+ "is_murderer": true,
17
+ "motive": "Embezzlement discovered",
18
+ "true_location": "Crime scene",
19
+ "alibi_story": "At home watching Netflix",
20
+ "phone_number": "+1-555-0101",
21
+ "bio": "Detail-oriented and ambitious. Has been with the company since day one."
22
+ },
23
+ {
24
+ "id": "suspect_2",
25
+ "name": "David Park",
26
+ "role": "Janitor",
27
+ "is_murderer": false,
28
+ "motive": "None",
29
+ "true_location": "2 blocks away",
30
+ "alibi_story": "Working late on 5th floor",
31
+ "phone_number": "+1-555-0102",
32
+ "bio": "Quiet and keeps to himself. Has a record for petty theft from 10 years ago."
33
+ },
34
+ {
35
+ "id": "suspect_3",
36
+ "name": "Emily Chen",
37
+ "role": "Victim's Sister",
38
+ "is_murderer": false,
39
+ "motive": "Inheritance dispute",
40
+ "true_location": "Restaurant downtown",
41
+ "alibi_story": "Dinner with friends",
42
+ "phone_number": "+1-555-0103",
43
+ "bio": "Emotional and protective of the family legacy. Argued with Marcus recently."
44
+ },
45
+ {
46
+ "id": "suspect_4",
47
+ "name": "Alex Rivera",
48
+ "role": "Competitor CEO",
49
+ "is_murderer": false,
50
+ "motive": "Corporate rivalry",
51
+ "true_location": "Airport lounge",
52
+ "alibi_story": "Catching a flight to NY",
53
+ "phone_number": "+1-555-0104",
54
+ "bio": "Ruthless businessman. Recently lost a major contract to Marcus's firm."
55
+ }
56
+ ],
57
+ "evidence": {
58
+ "location_data": {
59
+ "suspect_1_phone": {
60
+ "8:47 PM": {"lat": 37.7749, "lng": -122.4194, "location": "Crime scene"},
61
+ "8:50 PM": {"lat": 37.7750, "lng": -122.4200, "location": "0.1 mi away"}
62
+ },
63
+ "suspect_2_phone": {
64
+ "8:47 PM": {"lat": 37.7800, "lng": -122.4300, "location": "2 blocks away"}
65
+ },
66
+ "suspect_3_phone": {
67
+ "8:47 PM": {"lat": 37.7900, "lng": -122.4000, "location": "Downtown Restaurant"}
68
+ },
69
+ "suspect_4_phone": {
70
+ "8:47 PM": {"lat": 37.6213, "lng": -122.3790, "location": "SFO Airport"}
71
+ }
72
+ },
73
+ "footage_data": {
74
+ "10th_floor_camera": {
75
+ "8:45-8:50 PM": {
76
+ "visible_people": ["Person in black hoodie entering"],
77
+ "quality": "Grainy",
78
+ "key_frame": "8:47 PM - figure strikes victim with trophy"
79
+ }
80
+ },
81
+ "lobby_camera": {
82
+ "8:40-8:55 PM": {
83
+ "visible_people": ["Suspect 1 entering at 8:43", "Suspect 1 leaving at 8:52"],
84
+ "notes": "Suspect changed clothes, looks flustered"
85
+ }
86
+ }
87
+ },
88
+ "dna_evidence": {
89
+ "trophy_weapon": {
90
+ "primary_match": "suspect_1",
91
+ "confidence": "95%",
92
+ "notes": "Clear fingerprints found on the base"
93
+ },
94
+ "door_handle": {
95
+ "matches": ["suspect_1", "suspect_2", "victim"],
96
+ "notes": "Multiple people touched door recently"
97
+ }
98
+ },
99
+ "alibis": {
100
+ "suspect_1_alibi": {
101
+ "contact": "Nobody (claims was alone)",
102
+ "verifiable": false,
103
+ "truth": "Lying - was at crime scene"
104
+ },
105
+ "suspect_2_alibi": {
106
+ "contact": "+1-555-0199",
107
+ "contact_name": "Security Guard Tom",
108
+ "verifiable": true,
109
+ "truth": "Telling truth - saw him on 5th floor"
110
+ },
111
+ "suspect_3_alibi": {
112
+ "contact": "+1-555-0200",
113
+ "contact_name": "Waiter at La Casa",
114
+ "verifiable": true,
115
+ "truth": "Telling truth - bill was paid at 9:00 PM"
116
+ },
117
+ "suspect_4_alibi": {
118
+ "contact": "+1-555-0201",
119
+ "contact_name": "Airline Desk",
120
+ "verifiable": true,
121
+ "truth": "Telling truth - checked in at 8:30 PM"
122
+ }
123
+ }
124
+ },
125
+ "timeline": {
126
+ "8:30 PM": "Victim stays late working",
127
+ "8:43 PM": "Suspect 1 enters building",
128
+ "8:45 PM": "Suspect 2 seen on 5th floor",
129
+ "8:47 PM": "Murder occurs",
130
+ "8:52 PM": "Suspect 1 leaves building"
131
+ }
132
+ }
test_game.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from game import game_engine
2
+ import time
3
+
4
+ def test_game_logic():
5
+ print("Starting game test...")
6
+ session_id, game = game_engine.start_game("medium")
7
+ print(f"Game started. Session ID: {session_id}")
8
+ print(f"Scenario: {game.scenario['title']}")
9
+
10
+ # Test Agent Creation
11
+ print("Checking agents...")
12
+ for suspect in game.scenario["suspects"]:
13
+ agent = game.llm_manager.get_agent(suspect["id"])
14
+ if agent:
15
+ print(f"Agent created for {suspect['name']}")
16
+ else:
17
+ print(f"FAILED to create agent for {suspect['name']}")
18
+
19
+ # Test Questioning (Mock mode likely if no key)
20
+ print("\nTesting Questioning...")
21
+ suspect_id = game.scenario["suspects"][0]["id"]
22
+ response = game.question_suspect(suspect_id, "Where were you?")
23
+ print(f"Response from {suspect_id}: {response}")
24
+
25
+ # Test Tools
26
+ print("\nTesting Tools...")
27
+ # Use a valid phone number from scenario
28
+ phone = game.scenario["suspects"][0]["phone_number"]
29
+ res = game.use_tool("get_location", phone_number=phone, timestamp="8:47 PM")
30
+ print(f"Tool Result: {res}")
31
+
32
+ if "error" in res:
33
+ print("Tool test FAILED (unless expected error)")
34
+
35
+ # Test Round Advance
36
+ print("\nTesting Round Advance...")
37
+ print(f"Current Round: {game.round}")
38
+ game.advance_round()
39
+ print(f"New Round: {game.round}")
40
+
41
+ print("\nTest Complete.")
42
+
43
+ if __name__ == "__main__":
44
+ test_game_logic()
ui/__init__.py ADDED
File without changes
ui/components.py ADDED
File without changes
ui/styles.css ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .evidence-board {
2
+ background-color: #f0f0f0;
3
+ padding: 20px;
4
+ border-radius: 10px;
5
+ border: 2px solid #d0d0d0;
6
+ min-height: 300px;
7
+ }
8
+
9
+ .suspect-card {
10
+ background: linear-gradient(135deg, #2b32b2 0%, #1488cc 100%);
11
+ border-radius: 15px;
12
+ padding: 15px;
13
+ color: white;
14
+ margin-bottom: 10px;
15
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
16
+ }
17
+
18
+ .suspect-card h3 {
19
+ margin-top: 0;
20
+ color: #fff;
21
+ }
22
+
23
+ .suspect-card p {
24
+ font-size: 0.9em;
25
+ opacity: 0.9;
26
+ }
27
+
28
+ .chat-message {
29
+ font-family: 'Courier New', monospace;
30
+ }