GroupZHU/test.py

442 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()