import gradio as gr import os import json import uvicorn from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from game import game_engine from pydantic import BaseModel # --- Setup FastAPI for Static Files --- app = FastAPI() # Ensure directories exist os.makedirs("ui/static", exist_ok=True) app.mount("/static", StaticFiles(directory="ui/static"), name="static") # --- API Bridge --- class BridgeRequest(BaseModel): action: str data: dict = {} @app.post("/api/bridge") async def api_bridge(request: BridgeRequest): """Direct API endpoint for game logic communication.""" input_data = json.dumps({"action": request.action, "data": request.data}) print(f"API Bridge Received: {input_data}") response = session.handle_input(input_data) return response or {} # --- Game Logic Wrapper --- class GameSession: def __init__(self): self.session_id = None self.game = None def start(self, difficulty="medium"): self.session_id, self.game = game_engine.start_game(difficulty) return self._get_init_data() def _get_init_data(self): if not self.game: return None # Prepare static data for tools cameras = list(self.game.scenario["evidence"]["footage_data"].keys()) dna_map = {} for k, v in self.game.scenario["evidence"]["dna_evidence"].items(): dna_map[k] = v.get("label", k) # Fallback to ID if no label return { "action": "init_game", "data": { "scenario": self.game.scenario, "round": self.game.round, "points": self.game.points, "available_cameras": cameras, "dna_map": dna_map, "unlocked_evidence": self.game.unlocked_evidence } } def handle_input(self, input_json): if not input_json: return None try: data = json.loads(input_json) except: return None action = data.get("action") payload = data.get("data", {}) if action == "ready": # Wait for explicit start from Gradio UI, or return existing state if self.game: return self._get_init_data() return None # Wait for user to pick case if not self.game: return None if action == "select_suspect": return None if action == "next_round": if self.game.advance_round(): return { "action": "update_status", "data": { "round": self.game.round, "points": self.game.points } } else: return { "action": "game_over", "data": { "message": "COLD CASE. You ran out of time.", "verdict": False } } if action == "chat_message": suspect_id = payload.get("suspect_id") message = payload.get("message") response = self.game.question_suspect(suspect_id, message) suspect_name = next((s["name"] for s in self.game.scenario["suspects"] if s["id"] == suspect_id), "Suspect") return { "action": "update_chat", "data": { "role": "suspect", "name": suspect_name, "content": response } } if action == "use_tool": tool_name = payload.get("tool") arg = payload.get("input") # Default for single-input tools if tool_name == "accuse": result = self.game.make_accusation(payload.get("suspect_id")) if result["result"] == "win": return { "action": "game_over", "data": { "message": result["message"], "verdict": True } } elif result["result"] == "loss": return { "action": "game_over", "data": { "message": result["message"], "verdict": False } } else: return { "action": "round_failure", "data": { "message": result["message"], "eliminated_id": result["eliminated_id"], "round": result["new_round"], "points": result["new_points"] } } kwargs = {} if tool_name == "get_location": kwargs = {"phone_number": arg} elif tool_name == "call_alibi": # Support both simple string (old) and structured (new) if "alibi_id" in payload: arg = payload.get("alibi_id") # Update arg for formatter kwargs = { "alibi_id": arg, "question": payload.get("question") } else: kwargs = {"phone_number": arg} # Fallback elif tool_name == "get_dna_test": kwargs = {"evidence_id": arg} elif tool_name == "get_footage": kwargs = {"location": arg} result = self.game.use_tool(tool_name, **kwargs) if "error" in result: return { "action": "tool_error", "data": {"message": result["error"]} } # Format the result nicely evidence_data = format_tool_response(tool_name, arg, result, self.game.scenario) # Include updated points and unlocks in response evidence_data["updated_points"] = self.game.points evidence_data["unlocked_evidence"] = self.game.unlocked_evidence if "newly_unlocked" in result and result["newly_unlocked"]: evidence_data["newly_unlocked"] = result["newly_unlocked"] return { "action": "add_evidence", "data": evidence_data } return None def format_tool_response(tool_name, arg, result, scenario): """Formats tool output into HTML and finds associated suspect.""" suspect_id = None suspect_name = None html = "" title = f"Tool: {tool_name}" # Helpers to find suspect def find_by_phone(phone): clean_input = "".join(filter(str.isdigit, str(phone))) for s in scenario["suspects"]: s_phone = "".join(filter(str.isdigit, str(s.get("phone_number", "")))) if clean_input and s_phone.endswith(clean_input): return s return None def find_by_name(name): for s in scenario["suspects"]: if s["name"].lower() == name.lower(): return s return None def find_by_alibi_id(aid): for s in scenario["suspects"]: if s.get("alibi_id") == aid: return s return None # Logic per tool if tool_name == "get_location": suspect = find_by_phone(arg) if suspect: suspect_id = suspect["id"] suspect_name = suspect["name"] title = f"📍 Location Data" if "history" in result: html += "