Initial commit: Add murder mystery game with DeepSeek API integration
This commit is contained in:
commit
35af6af298
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
.DS_Store
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
82
README.md
Normal file
82
README.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# 密室杀人侦破 - 网页推理游戏
|
||||||
|
|
||||||
|
一个基于AI生成的密室杀人推理游戏,玩家需要通过选择不同的选项来推进案件调查,最终达到100%进度结案。
|
||||||
|
|
||||||
|
## 项目特色
|
||||||
|
|
||||||
|
- 🎮 **20个独立场景**,每个场景4个选项
|
||||||
|
- 🤖 **AI生成剧情**,使用DeepSeek API自动生成场景和选项
|
||||||
|
- ⚠️ **危机系统**,连续危险选择会触发特殊状态
|
||||||
|
- 🏆 **多结局系统**,根据选择决定案件结局
|
||||||
|
- 📱 **响应式设计**,支持移动端和桌面端
|
||||||
|
- 💾 **进度保存**,自动保存游戏状态
|
||||||
|
|
||||||
|
## 游戏机制
|
||||||
|
|
||||||
|
### 选项类型
|
||||||
|
- **正确选项**:正常推进剧情,进度+5~10%
|
||||||
|
- **错误选项**:可能触发坏结局,进度+5~10%
|
||||||
|
- **未知选项**:进度不变,需要谨慎判断
|
||||||
|
- **危险选项**:可能触发最坏结局,进度+8~10%
|
||||||
|
|
||||||
|
### 危机系统
|
||||||
|
连续选择3次危险选项会触发危机状态:
|
||||||
|
- **紧张**:增加2个无关选项
|
||||||
|
- **焦虑**:选项文本可能被隐藏
|
||||||
|
- **恐慌**:界面特效和文字抖动
|
||||||
|
|
||||||
|
### 结局判定
|
||||||
|
- **好结局**:正确找出真凶
|
||||||
|
- **坏结局**:找到表面罪犯(错误+危险选择≥7次)
|
||||||
|
- **最坏结局**:被误认为罪犯(危险选择≥4次)
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
密室杀人侦破/
|
||||||
|
├── index.html # 主页面
|
||||||
|
├── styles.css # 样式文件
|
||||||
|
├── game.js # 游戏逻辑
|
||||||
|
├── config.js # 配置文件
|
||||||
|
├── plan1.md # 详细开发计划
|
||||||
|
└── README.md # 项目说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
1. 直接打开 `index.html` 文件即可开始游戏
|
||||||
|
2. 点击"开始推理"按钮进入游戏
|
||||||
|
3. 阅读场景描述,选择你认为正确的选项
|
||||||
|
4. 观察进度条变化,谨慎选择避免触发危机
|
||||||
|
5. 达到100%进度时查看案件结局
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **前端**: HTML5, CSS3, JavaScript (ES6+)
|
||||||
|
- **样式**: 响应式设计,CSS动画
|
||||||
|
- **API**: DeepSeek AI API
|
||||||
|
- **存储**: LocalStorage
|
||||||
|
|
||||||
|
## 安全说明
|
||||||
|
|
||||||
|
⚠️ **重要**: 当前版本在前端代码中硬编码了API密钥,这在实际部署中是不安全的。建议:
|
||||||
|
|
||||||
|
1. 使用环境变量管理API密钥
|
||||||
|
2. 通过后端代理调用API
|
||||||
|
3. 部署时移除硬编码的密钥
|
||||||
|
|
||||||
|
## 开发计划
|
||||||
|
|
||||||
|
详细开发计划请参考 [plan1.md](plan1.md) 文件。
|
||||||
|
|
||||||
|
## 扩展功能
|
||||||
|
|
||||||
|
未来可考虑添加:
|
||||||
|
- 多案件系统
|
||||||
|
- 成就系统
|
||||||
|
- 社交分享功能
|
||||||
|
- 语音朗读功能
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目仅供学习使用。
|
||||||
14
plan.md
Normal file
14
plan.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
我正在制作一个网页游戏项目,名作“密室杀人侦破”,使用deepseek的api钥匙来自动生成足够长的剧情和选项对应的文案,其中DEEPSEEK_API_KEY=sk-f42302a1e8ff4825a19e313db79a391b
|
||||||
|
有三个主要功能:1,给出一个密室杀人案件,并且具有自动推进和演算剧情的效果,2,给出4个选项帮助玩家推进剧情,3,根据玩家的选项来推进整体进度,进度抵达100%时结案。
|
||||||
|
交互流程:用户点开网站,看见背景介绍和案件基本信息,点击“开始推理”可以正式开始。推理过程中可以选择4个选项中的一个进行行动。每个选项都有不同的效果,会导致进度的推进或倒退。玩家需要根据当前的情况和选项的效果来判断下一步该选哪个选项。当进度抵达100%时,会显示案件的结论。
|
||||||
|
总计20个场景,每个场景有4个选项,玩家不知道每个选项的具体效果,只能根据选项和剧情的文字来判断。每个选项都有不同的效果。分别为:
|
||||||
|
1,正确选项:进度增加5到10%,玩家可以继续推理。
|
||||||
|
2,错误选项:进度增加5到10%,结局可能会出现坏结局选项,玩家需要更加谨慎选择。
|
||||||
|
3,未知:进度保持不变,玩家需要更加注意自己的选择。
|
||||||
|
4,危险:进度增加8到10%,结局可能出现最坏的情况,玩家需要更加谨慎选择。
|
||||||
|
当玩家连续选择危险选项超过3次时,会触发危机提示,提示玩家注意自己的选择,并且随机触发以下负面效果:
|
||||||
|
1,紧张:增加无关选项两个,玩家需要更加谨慎选择。
|
||||||
|
2,焦虑:所有选项成功率趋于平均化,玩家需要更加注意自己的选择。
|
||||||
|
3,恐慌:修改界面颜色为黑色,红色还有白色,并且增加选项文字抖动效果。
|
||||||
|
当结局场景时,只会出现一个选项,若错误选项和危险选项被选择的次数总计超过7次,则好结局选项被替换坏结局选项,同时若危险选项被选择的次数超过4次,则额外导致坏结局选项被替换成最坏的结局选项。
|
||||||
|
好结局为你成功辨别了真凶,坏结局为你找到了表面上的罪犯,但似乎真凶另有其人,最坏结局为你不但没有找出真凶,还被其他人当作罪犯。
|
||||||
235
plan1.md
Normal file
235
plan1.md
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
# 密室杀人侦破 - 详细开发计划
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
**项目名称**: 密室杀人侦破
|
||||||
|
**项目类型**: 网页推理游戏
|
||||||
|
**核心功能**: 使用DeepSeek API自动生成剧情和选项文案
|
||||||
|
**API密钥**: sk-f42302a1e8ff4825a19e313db79a391b
|
||||||
|
|
||||||
|
## 游戏核心机制
|
||||||
|
|
||||||
|
### 1. 基本游戏流程
|
||||||
|
- **场景数量**: 20个独立场景
|
||||||
|
- **选项数量**: 每个场景4个选项
|
||||||
|
- **进度系统**: 0-100%进度条,达到100%时结案
|
||||||
|
- **结局类型**: 好结局、坏结局、最坏结局
|
||||||
|
|
||||||
|
### 2. 选项类型与效果
|
||||||
|
|
||||||
|
#### 选项分类(玩家不可见)
|
||||||
|
1. **正确选项**
|
||||||
|
- 进度增加: 5-10%
|
||||||
|
- 效果: 正常推进剧情,无负面效果
|
||||||
|
|
||||||
|
2. **错误选项**
|
||||||
|
- 进度增加: 5-10%
|
||||||
|
- 效果: 可能触发坏结局条件
|
||||||
|
|
||||||
|
3. **未知选项**
|
||||||
|
- 进度变化: 保持不变
|
||||||
|
- 效果: 需要玩家谨慎判断
|
||||||
|
|
||||||
|
4. **危险选项**
|
||||||
|
- 进度增加: 8-10%
|
||||||
|
- 效果: 可能触发最坏结局条件
|
||||||
|
|
||||||
|
### 3. 危机系统
|
||||||
|
|
||||||
|
#### 触发条件
|
||||||
|
- 连续选择危险选项 ≥ 3次
|
||||||
|
|
||||||
|
#### 危机效果(随机触发一种)
|
||||||
|
1. **紧张状态**
|
||||||
|
- 增加2个无关选项
|
||||||
|
- 选项总数变为6个
|
||||||
|
|
||||||
|
2. **焦虑状态**
|
||||||
|
- 所有选项文本有概率变为黑框不可见状态
|
||||||
|
- 降低判断难度
|
||||||
|
|
||||||
|
3. **恐慌状态**
|
||||||
|
- 界面颜色变为黑、红、白
|
||||||
|
- 选项文字出现抖动效果
|
||||||
|
|
||||||
|
### 4. 结局判定系统
|
||||||
|
|
||||||
|
#### 结局条件
|
||||||
|
- **好结局**: 正确辨别真凶
|
||||||
|
- **坏结局**: 找到表面罪犯,真凶另有其人
|
||||||
|
- **最坏结局**: 被误认为罪犯
|
||||||
|
|
||||||
|
#### 结局触发规则
|
||||||
|
- 错误 + 危险选项选择次数 ≥ 7次: 好结局 → 坏结局
|
||||||
|
- 危险选项选择次数 ≥ 4次: 坏结局 → 最坏结局
|
||||||
|
|
||||||
|
## 技术架构设计
|
||||||
|
|
||||||
|
### 1. 前端技术栈
|
||||||
|
- **框架**: React/Vue.js
|
||||||
|
- **样式**: CSS3 + 动画效果
|
||||||
|
- **状态管理**: Redux/Vuex
|
||||||
|
- **路由**: React Router/Vue Router
|
||||||
|
|
||||||
|
### 2. 后端技术栈
|
||||||
|
- **语言**: Node.js/Python
|
||||||
|
- **API集成**: DeepSeek API调用
|
||||||
|
- **数据存储**: 本地存储/简单数据库
|
||||||
|
|
||||||
|
### 3. API集成设计
|
||||||
|
|
||||||
|
#### DeepSeek API调用策略
|
||||||
|
```javascript
|
||||||
|
// 场景生成API调用
|
||||||
|
const generateScene = async (currentProgress, previousChoices) => {
|
||||||
|
const prompt = `
|
||||||
|
生成第${currentScene}个密室杀人场景:
|
||||||
|
当前进度:${currentProgress}%
|
||||||
|
玩家选择历史:${previousChoices}
|
||||||
|
请生成:
|
||||||
|
1. 场景描述(200-300字)
|
||||||
|
2. 4个选项文案(每个选项20-30字)
|
||||||
|
3. 选项类型标注(正确/错误/未知/危险)
|
||||||
|
`;
|
||||||
|
|
||||||
|
return await callDeepSeekAPI(prompt);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 剧情连贯性保证
|
||||||
|
- 保存玩家选择历史
|
||||||
|
- 基于历史生成连贯剧情
|
||||||
|
- 维护角色关系和证据链
|
||||||
|
|
||||||
|
## 用户界面设计
|
||||||
|
|
||||||
|
### 1. 页面结构
|
||||||
|
1. **首页**
|
||||||
|
- 游戏标题和背景介绍
|
||||||
|
- 案件基本信息
|
||||||
|
- "开始推理"按钮
|
||||||
|
|
||||||
|
2. **游戏主界面**
|
||||||
|
- 顶部: 进度条显示
|
||||||
|
- 中部: 当前场景描述
|
||||||
|
- 底部: 4个选项按钮
|
||||||
|
- 侧边: 证据收集区域(可选)
|
||||||
|
|
||||||
|
3. **结局页面**
|
||||||
|
- 案件结论展示
|
||||||
|
- 结局评价
|
||||||
|
- 重新开始按钮
|
||||||
|
|
||||||
|
### 2. 视觉设计
|
||||||
|
- **主色调**: 暗色系(营造神秘氛围)
|
||||||
|
- **字体**: 易读的衬线字体
|
||||||
|
- **动画**: 平滑的过渡效果
|
||||||
|
- **响应式**: 适配移动端和桌面端
|
||||||
|
|
||||||
|
## 开发阶段规划
|
||||||
|
|
||||||
|
### 第一阶段:基础框架搭建(1-2周)
|
||||||
|
- [ ] 项目初始化
|
||||||
|
- [ ] 基础页面结构
|
||||||
|
- [ ] 简单的状态管理
|
||||||
|
- [ ] DeepSeek API集成测试
|
||||||
|
|
||||||
|
### 第二阶段:核心功能实现(2-3周)
|
||||||
|
- [ ] 场景生成系统
|
||||||
|
- [ ] 选项选择逻辑
|
||||||
|
- [ ] 进度管理系统
|
||||||
|
- [ ] 基础UI界面
|
||||||
|
|
||||||
|
### 第三阶段:高级功能开发(2周)
|
||||||
|
- [ ] 危机系统实现
|
||||||
|
- [ ] 结局判定系统
|
||||||
|
- [ ] 视觉特效(恐慌状态)
|
||||||
|
- [ ] 数据持久化
|
||||||
|
|
||||||
|
### 第四阶段:优化和测试(1周)
|
||||||
|
- [ ] 性能优化
|
||||||
|
- [ ] 用户体验测试
|
||||||
|
- [ ] Bug修复
|
||||||
|
- [ ] 部署上线
|
||||||
|
|
||||||
|
## 数据结构和状态管理
|
||||||
|
|
||||||
|
### 1. 游戏状态对象
|
||||||
|
```javascript
|
||||||
|
const gameState = {
|
||||||
|
currentScene: 1,
|
||||||
|
progress: 0,
|
||||||
|
choicesHistory: [],
|
||||||
|
dangerCount: 0,
|
||||||
|
errorCount: 0,
|
||||||
|
crisisActive: false,
|
||||||
|
crisisType: null,
|
||||||
|
endingType: 'good' // good/bad/worst
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 场景数据结构
|
||||||
|
```javascript
|
||||||
|
const sceneData = {
|
||||||
|
id: 1,
|
||||||
|
description: "场景描述文本",
|
||||||
|
options: [
|
||||||
|
{ text: "选项1", type: "correct", effect: 8 },
|
||||||
|
{ text: "选项2", type: "danger", effect: 10 },
|
||||||
|
{ text: "选项3", type: "unknown", effect: 0 },
|
||||||
|
{ text: "选项4", type: "error", effect: 6 }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全注意事项
|
||||||
|
|
||||||
|
### API密钥安全
|
||||||
|
- 不要在前端代码中硬编码API密钥
|
||||||
|
- 使用环境变量或后端代理
|
||||||
|
- 考虑使用服务器端API调用
|
||||||
|
|
||||||
|
### 用户数据保护
|
||||||
|
- 仅存储必要的游戏进度数据
|
||||||
|
- 不收集个人敏感信息
|
||||||
|
- 提供数据清除功能
|
||||||
|
|
||||||
|
## 扩展功能(可选)
|
||||||
|
|
||||||
|
### 1. 多案件系统
|
||||||
|
- 不同难度级别的案件
|
||||||
|
- 随机生成案件内容
|
||||||
|
- 案件收藏和重玩功能
|
||||||
|
|
||||||
|
### 2. 成就系统
|
||||||
|
- 完美推理成就
|
||||||
|
- 快速通关成就
|
||||||
|
- 特殊结局成就
|
||||||
|
|
||||||
|
### 3. 社交功能
|
||||||
|
- 成绩分享
|
||||||
|
- 排行榜系统
|
||||||
|
- 好友挑战
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
### 技术风险
|
||||||
|
1. **API稳定性**: DeepSeek API服务中断
|
||||||
|
- 应对: 本地缓存备用剧情
|
||||||
|
|
||||||
|
2. **生成质量**: AI生成内容不符合预期
|
||||||
|
- 应对: 人工审核+内容过滤
|
||||||
|
|
||||||
|
### 用户体验风险
|
||||||
|
1. **难度平衡**: 游戏过于简单或困难
|
||||||
|
- 应对: 动态难度调整机制
|
||||||
|
|
||||||
|
2. **剧情连贯性**: 场景之间逻辑断裂
|
||||||
|
- 应对: 强化上下文关联生成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**项目负责人**: 朱鹏翰
|
||||||
|
**预计完成时间**: 6-8周
|
||||||
|
**技术难度**: 中等
|
||||||
|
**创新点**: AI生成剧情 + 动态结局系统
|
||||||
7
pyproject.toml
Normal file
7
pyproject.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[project]
|
||||||
|
name = "ai-work"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = []
|
||||||
442
test.py
Normal file
442
test.py
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
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()
|
||||||
Loading…
Reference in New Issue
Block a user