import streamlit as st import os from openai import OpenAI from dotenv import load_dotenv import json import time from typing import Dict, List, Optional load_dotenv() client = OpenAI(api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com") # 全局状态管理 if 'current_scene' not in st.session_state: st.session_state.current_scene = 1 if 'previous_choices' not in st.session_state: st.session_state.previous_choices = [] if 'scene_data' not in st.session_state: st.session_state.scene_data = None if 'game_ended' not in st.session_state: st.session_state.game_ended = False if 'ending_type' not in st.session_state: st.session_state.ending_type = None def determine_ending(choices_history: List[Dict]) -> str: """根据选择历史判定结局类型""" # 统计选项类型数量 error_count = 0 danger_count = 0 for choice in choices_history: if choice["option_type"] == "error": error_count += 1 elif choice["option_type"] == "danger": danger_count += 1 # 根据plan1.md中的规则判定结局 # 规则1: 错误 + 危险选项选择次数 ≥ 7次: 好结局 → 坏结局 # 规则2: 危险选项选择次数 ≥ 4次: 坏结局 → 最坏结局 if danger_count >= 4: return "worst" # 最坏结局 elif (error_count + danger_count) >= 7: return "bad" # 坏结局 else: return "good" # 好结局 def get_ending_description(ending_type: str) -> Dict: """获取结局描述""" endings = { "good": { "title": "🎉 完美破案 - 好结局", "description": """ 经过缜密的调查,你成功找出了真凶! **案件真相**: 真凶是李明的商业合作伙伴王强。由于商业纠纷,王强利用李明书房内的密道进入现场作案。 现场发现的财务文件揭示了两人之间的利益冲突,而书房内的隐藏摄像头记录下了关键证据。 **评价**: 你的推理能力和观察力令人钦佩,成功还原了案件真相! """, "color": "success" }, "bad": { "title": "😔 误判真凶 - 坏结局", "description": """ 你找到了表面上的罪犯,但真凶另有其人。 **案件真相**: 你错误地将管家李叔认定为凶手,但实际上真凶是李明的妻子张美丽。 她因财产纠纷策划了这起谋杀,并巧妙地将嫌疑转移给了管家。 **评价**: 虽然找到了部分线索,但关键的证据被忽略了,真凶逍遥法外。 """, "color": "warning" }, "worst": { "title": "💀 被误认为罪犯 - 最坏结局", "description": """ 由于错误的调查方向,你被误认为是凶手! **案件真相**: 你的调查行为引起了警方的怀疑,现场留下的指纹和监控录像被错误解读。 真凶趁机逃脱,而你却成为了替罪羊。 **评价**: 过于冒险的调查选择导致了严重的后果,需要更加谨慎地处理案件。 """, "color": "error" } } return endings.get(ending_type, endings["good"]) class GameSceneGenerator: """游戏场景生成器""" def __init__(self): self.api_key = os.getenv("DEEPSEEK_API_KEY") if not self.api_key: st.error("未找到DEEPSEEK_API_KEY环境变量") st.stop() self.client = OpenAI(api_key=self.api_key, base_url="https://api.deepseek.com") self.model = "deepseek-chat" def generate_scene(self, scene_number: int, previous_choices: List[str] = None) -> Dict: """生成游戏场景""" if previous_choices is None: previous_choices = [] # 构建提示词 prompt = self._build_prompt(scene_number, previous_choices) try: # 调用DeepSeek API response = self._call_api(prompt) # 解析响应 scene_data = self._parse_response(response, scene_number) return scene_data except Exception as e: st.error(f"生成场景失败: {e}") return self._get_fallback_scene(scene_number) def _build_prompt(self, scene_number: int, previous_choices: List[str]) -> str: """构建API提示词""" history_text = "" if previous_choices: history_text = f"玩家之前的调查选择:{', '.join(previous_choices)}" prompt = f""" 请为密室杀人推理游戏生成第{scene_number}个场景。 案件背景:富豪李明在书房被谋杀,现场是密室状态。门窗从内部锁住,没有明显的外部入侵痕迹。 {history_text} 请生成以下内容: 1. 场景描述(150-250字):详细描述当前调查现场的环境、发现的线索和可疑之处 2. 4个调查选项:每个选项应该是合理的调查行动,长度20-40字 选项类型说明: - 正确选项:能够推进案件调查的正确选择 - 错误选项:看似合理但会误导调查的选择 - 危险选项:可能带来风险或触发坏结局的选择 - 未知选项:结果不确定,需要玩家谨慎判断的选择 请严格按照以下JSON格式返回: {{ "scene_number": {scene_number}, "description": "详细的场景描述文本", "options": [ {{"text": "选项1文本", "type": "correct"}}, {{"text": "选项2文本", "type": "error"}}, {{"text": "选项3文本", "type": "danger"}}, {{"text": "选项4文本", "type": "unknown"}} ] }} 请确保: - 场景描述符合推理游戏的氛围 - 选项文本清晰明确,具有可操作性 - JSON格式正确无误 """ return prompt def _call_api(self, prompt: str) -> str: """调用DeepSeek API""" try: response = self.client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": "你是一个专业的推理游戏设计师,擅长创作悬疑推理场景和调查选项。"}, {"role": "user", "content": prompt} ], max_tokens=2000, temperature=0.7 ) return response.choices[0].message.content except Exception as e: raise Exception(f"API调用失败: {e}") def _parse_response(self, response: str, scene_number: int) -> Dict: """解析API响应""" # 尝试从响应中提取JSON try: # 查找JSON开始和结束位置 start_idx = response.find('{') end_idx = response.rfind('}') + 1 if start_idx == -1 or end_idx == 0: raise ValueError("响应中未找到有效的JSON数据") json_str = response[start_idx:end_idx] scene_data = json.loads(json_str) # 验证必需字段 required_fields = ["scene_number", "description", "options"] for field in required_fields: if field not in scene_data: raise ValueError(f"缺少必需字段: {field}") # 验证选项格式 if len(scene_data["options"]) != 4: raise ValueError("选项数量必须为4个") for option in scene_data["options"]: if "text" not in option or "type" not in option: raise ValueError("选项格式不正确") return scene_data except Exception as e: st.warning(f"JSON解析失败,使用备用场景: {e}") return self._get_fallback_scene(scene_number) def _get_fallback_scene(self, scene_number: int) -> Dict: """获取备用场景(当API失败时使用)""" fallback_scenes = { 1: { "scene_number": 1, "description": "你站在富豪李明书房门口。书房门紧闭,透过门缝可以看到里面一片狼藉。书桌上的文件散落一地,一盏台灯倒在桌角。空气中弥漫着淡淡的血腥味。", "options": [ {"text": "仔细检查书房门锁和周围环境", "type": "correct"}, {"text": "立即破门而入查看情况", "type": "danger"}, {"text": "先询问管家案发时的情况", "type": "error"}, {"text": "检查窗户是否从内部锁住", "type": "unknown"} ] }, 2: { "scene_number": 2, "description": "进入书房后,你看到李明倒在书桌旁的地毯上,胸口插着一把匕首。书房窗户紧闭,窗帘半拉着。书桌上散落着一些财务报表和信件。", "options": [ {"text": "仔细检查尸体和凶器上的指纹", "type": "correct"}, {"text": "立即搜查书房寻找隐藏线索", "type": "danger"}, {"text": "询问第一个发现尸体的人", "type": "error"}, {"text": "检查书房内的监控设备", "type": "unknown"} ] } } return fallback_scenes.get(scene_number, fallback_scenes[1]) def main(): """主函数 - Streamlit应用""" st.set_page_config( page_title="密室杀人侦破 - 推理游戏", page_icon="🔍", layout="wide" ) # 初始化生成器 generator = GameSceneGenerator() # 游戏标题 st.title("🔍 密室杀人侦破") st.markdown("---") # 游戏状态显示 col_status1, col_status2, col_status3 = st.columns([1, 1, 1]) with col_status1: st.metric("当前场景", st.session_state.current_scene) with col_status2: st.metric("选择记录", len(st.session_state.previous_choices)) with col_status3: progress = min((st.session_state.current_scene - 1) * 5, 100) st.metric("调查进度", f"{progress}%") st.markdown("---") # 游戏主界面 if st.session_state.game_ended: # 显示结局页面 ending_info = get_ending_description(st.session_state.ending_type) st.subheader(ending_info["title"]) if ending_info["color"] == "success": st.success(ending_info["description"]) elif ending_info["color"] == "warning": st.warning(ending_info["description"]) else: st.error(ending_info["description"]) st.markdown("---") # 显示统计信息 col1, col2, col3 = st.columns(3) with col1: total_choices = len(st.session_state.previous_choices) st.metric("总选择次数", total_choices) with col2: error_count = sum(1 for c in st.session_state.previous_choices if c["option_type"] == "error") st.metric("错误选择", error_count) with col3: danger_count = sum(1 for c in st.session_state.previous_choices if c["option_type"] == "danger") st.metric("危险选择", danger_count) # 重新开始按钮 if st.button("🔄 重新开始游戏", use_container_width=True, type="primary"): st.session_state.current_scene = 1 st.session_state.previous_choices = [] st.session_state.scene_data = None st.session_state.game_ended = False st.session_state.ending_type = None st.rerun() elif st.session_state.scene_data is None: # 显示开始界面 st.subheader("案件背景") st.info(""" 富豪李明在书房被谋杀,现场是密室状态。门窗从内部锁住,没有明显的外部入侵痕迹。 书房内发现李明倒在书桌旁,胸口插着一把匕首。现场没有发现凶手的明显痕迹。 你需要通过调查各个场景,收集线索,最终找出真凶。 """) if st.button("🚀 开始调查", use_container_width=True, type="primary"): with st.spinner("正在生成第一个场景..."): # 生成场景一 scene_data = generator.generate_scene(1) st.session_state.scene_data = scene_data st.session_state.current_scene = 1 st.rerun() else: # 显示当前场景 scene_data = st.session_state.scene_data # 显示场景描述 st.subheader(f"场景 {st.session_state.current_scene}") st.write(scene_data["description"]) st.markdown("---") # 显示选项(统一外观) st.subheader("调查行动") # 场景20的特殊处理 if st.session_state.current_scene == 20: # 显示"最后推断"按钮 if st.button("🔍 最后推断", use_container_width=True, type="primary"): # 判定结局 ending_type = determine_ending(st.session_state.previous_choices) st.session_state.ending_type = ending_type st.session_state.game_ended = True st.rerun() else: # 正常场景的选项按钮 for i, option in enumerate(scene_data["options"], 1): # 使用统一的按钮样式,隐藏类型信息 if st.button( f"{option['text']}", key=f"option_{i}_{st.session_state.current_scene}", use_container_width=True ): # 记录选择 choice_record = { "scene": st.session_state.current_scene, "option_text": option["text"], "option_type": option["type"] } st.session_state.previous_choices.append(choice_record) # 生成下一场景 with st.spinner("正在进入下一场景..."): next_scene = st.session_state.current_scene + 1 if next_scene <= 20: new_scene_data = generator.generate_scene( next_scene, [choice["option_text"] for choice in st.session_state.previous_choices] ) st.session_state.scene_data = new_scene_data st.session_state.current_scene = next_scene else: # 游戏结束 st.session_state.scene_data = None st.session_state.current_scene = 1 st.session_state.previous_choices = [] st.rerun() # 显示选择历史 if st.session_state.previous_choices: st.markdown("---") st.subheader("调查记录") for i, choice in enumerate(st.session_state.previous_choices, 1): st.write(f"**场景{choice['scene']}**: {choice['option_text']}") # 侧边栏 - 游戏控制 st.sidebar.header("游戏控制") if st.session_state.scene_data or st.session_state.game_ended: if st.sidebar.button("🔄 重新开始", use_container_width=True): st.session_state.scene_data = None st.session_state.current_scene = 1 st.session_state.previous_choices = [] st.session_state.game_ended = False st.session_state.ending_type = None st.rerun() # 侧边栏 - API状态 st.sidebar.markdown("---") st.sidebar.subheader("API状态") if generator.api_key: st.sidebar.success("✅ API密钥已配置") else: st.sidebar.error("❌ API密钥未找到") st.sidebar.info(""" **游戏说明**: - 点击"开始调查"开始游戏 - 双击选项进入下一场景 - 所有选项外观一致,需要谨慎选择 - 选择记录会影响后续剧情发展 """) if __name__ == "__main__": main()