feat: add MMGame backend and frontend source code
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
import json
|
||||
import random
|
||||
import httpx
|
||||
from app.config import settings
|
||||
|
||||
|
||||
class AIBackend:
|
||||
def __init__(self):
|
||||
self.backend = settings.AI_BACKEND
|
||||
|
||||
async def chat(self, messages: list[dict], temperature: float = 0.7, max_tokens: int = 512) -> str:
|
||||
if self.backend == "ollama":
|
||||
return await self._ollama_chat(messages, temperature, max_tokens)
|
||||
elif self.backend == "deepseek":
|
||||
return await self._deepseek_chat(messages, temperature, max_tokens)
|
||||
else:
|
||||
return ""
|
||||
|
||||
async def _ollama_chat(self, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
resp = await client.post(
|
||||
f"{settings.OLLAMA_BASE_URL}/api/chat",
|
||||
json={
|
||||
"model": settings.OLLAMA_MODEL,
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
"options": {"temperature": temperature, "num_predict": max_tokens},
|
||||
},
|
||||
)
|
||||
data = resp.json()
|
||||
return data.get("message", {}).get("content", "")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
async def _deepseek_chat(self, messages: list[dict], temperature: float, max_tokens: int) -> str:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
resp = await client.post(
|
||||
f"{settings.DEEPSEEK_BASE_URL}/v1/chat/completions",
|
||||
headers={"Authorization": f"Bearer {settings.DEEPSEEK_API_KEY}"},
|
||||
json={"model": settings.DEEPSEEK_MODEL, "messages": messages, "temperature": temperature, "max_tokens": max_tokens},
|
||||
)
|
||||
result = resp.json()
|
||||
return result["choices"][0]["message"]["content"]
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
ai_backend = AIBackend()
|
||||
|
||||
|
||||
DM_INTROS = {
|
||||
"hardcore": "欢迎来到这场推理盛宴。真相就藏在你面前的线索之中,仔细观察,大胆推理。",
|
||||
"emotional": "这是一个关于人心的故事。有时候,真相并不重要,重要的是你在这个过程中感受到了什么。",
|
||||
"欢乐": "欢迎来到这场欢乐的聚会!记住,每个人都在演戏,但真相只有一个!",
|
||||
"恐怖": "黑暗中有什么在注视着你……准备好了吗?",
|
||||
}
|
||||
|
||||
|
||||
async def dm_generate(script: dict, phase_index: int, released_clues: list[str], context: str = "") -> dict:
|
||||
phases = script.get("phases", [])
|
||||
phase = phases[phase_index] if phase_index < len(phases) else phases[-1]
|
||||
title = script["title"]
|
||||
script_type = script.get("type", "hardcore")
|
||||
|
||||
type_intro = DM_INTROS.get(script_type, "")
|
||||
clues_str = ", ".join(released_clues) if released_clues else "暂无"
|
||||
|
||||
prompt = f"""你是一个剧本杀主持人(DM),请以主持人的身份发言。
|
||||
当前剧本: {title}
|
||||
当前阶段: {phase['name']}
|
||||
阶段描述: {phase['description']}
|
||||
已发放线索: {clues_str}
|
||||
上下文: {context}
|
||||
|
||||
请简短发言(50字以内),推动剧情发展。"""
|
||||
|
||||
result = await ai_backend.chat([{"role": "user", "content": prompt}])
|
||||
if result:
|
||||
# Try to parse as JSON
|
||||
cleaned = result.strip().removeprefix("```json").removesuffix("```").strip()
|
||||
try:
|
||||
return json.loads(cleaned)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return {"action": "narrate", "content": result}
|
||||
|
||||
voice = phase["description"][:80]
|
||||
if phase_index == 0 and context == "opening":
|
||||
voice = f"{type_intro}\n\n欢迎来到【{title}】。{script.get('background', '')[:100]}"
|
||||
return {"action": "narrate", "content": f"【{phase['name']}】{voice}"}
|
||||
|
||||
|
||||
async def npc_generate(role: dict, background: str, phase_name: str, recent_messages: list[str],
|
||||
player_message: str = "", is_private: bool = False,
|
||||
known_clues: list[str] = None, phase_goal: str = "",
|
||||
all_roles: list[dict] = None) -> dict:
|
||||
personality = role.get("personality", role.get("name", ""))
|
||||
clues_str = " | ".join(known_clues[-5:]) if known_clues else "暂无线索"
|
||||
roles_str = ""
|
||||
if all_roles:
|
||||
rlist = [r["name"] for r in all_roles if r["id"] != role.get("id")]
|
||||
roles_str = "其他玩家: " + ", ".join(rlist)
|
||||
|
||||
prompt = f"""你正在扮演一个剧本杀角色。请严格按照角色设定发言。
|
||||
|
||||
角色信息:
|
||||
- 姓名: {role['name']}
|
||||
- 公开身份: {role.get('publicProfile', '')}
|
||||
- 性格: {personality}
|
||||
- 你的目标: {role.get('goal', '')}
|
||||
- 你的秘密: {role.get('secretProfile', '')}
|
||||
|
||||
当前背景: {background[:80]}...
|
||||
当前阶段: {phase_name}
|
||||
{("阶段目标: " + phase_goal) if phase_goal else ""}
|
||||
{roles_str}
|
||||
|
||||
你已知道的线索:
|
||||
{clues_str}
|
||||
|
||||
最近发言:
|
||||
{' | '.join(recent_messages[-4:])}
|
||||
|
||||
{'【私聊】' + player_message if is_private and player_message else ''}
|
||||
{'【有人对你说】' + player_message if not is_private and player_message else ''}
|
||||
|
||||
【发言规则】
|
||||
1. 以角色身份说人话,简短自然(30字以内)
|
||||
2. 你的秘密绝对不能主动说出来
|
||||
3. 被问到时可以撒谎、回避、转移话题
|
||||
4. 结合已公开的线索来推理和回应
|
||||
5. 如果你知道某些线索的真相(如你就是凶手),可以故意误导他人
|
||||
|
||||
直接输出你的角色发言内容,不要JSON、不要解释。"""
|
||||
|
||||
result = await ai_backend.chat([{"role": "user", "content": prompt}])
|
||||
if result:
|
||||
cleaned = result.strip().removeprefix("```json").removesuffix("```").strip()
|
||||
try:
|
||||
return json.loads(cleaned)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if result.startswith("{"):
|
||||
return {"action": "speak", "content": result}
|
||||
return {"action": "speak", "content": result}
|
||||
|
||||
return {"action": "speak", "content": _rule_npc_reply(role, player_message, is_private, known_clues)}
|
||||
|
||||
|
||||
def _rule_npc_reply(role: dict, player_message: str, is_private: bool, known_clues: list[str] = None) -> str:
|
||||
name = role["name"]
|
||||
personality = role.get("personality", "")
|
||||
goal = role.get("goal", "")
|
||||
clues = known_clues or []
|
||||
|
||||
if not player_message:
|
||||
if clues:
|
||||
replies = [
|
||||
f"{name}沉思道:\"这些线索……我觉得需要重新梳理一下。\"",
|
||||
f"{name}说:\"我注意到了一些细节,但现在还不方便说。\"",
|
||||
f"{name}看了看法医报告:\"时间线和线索对不上,肯定有人撒谎。\"",
|
||||
]
|
||||
return random.choice(replies)
|
||||
replies = [
|
||||
f"{name}环顾四周,若有所思。",
|
||||
f"{name}清了清嗓子:\"各位,我觉得我们应该整理一下思路。\"",
|
||||
f"{name}沉默地看着大家。",
|
||||
f"{name}低声说:\"这件事没有那么简单……\"",
|
||||
]
|
||||
return random.choice(replies)
|
||||
|
||||
if "凶手" in player_message or "杀人" in player_message or "你杀" in player_message:
|
||||
denials = [
|
||||
f"{name}脸色一变:\"你凭什么这么说?证据呢?\"",
|
||||
f"{name}冷笑一声:\"如果我是凶手,我还会坐在这里?\"",
|
||||
f"{name}摇头:\"我没有理由杀他。\"",
|
||||
]
|
||||
return random.choice(denials)
|
||||
|
||||
if "时间" in player_message or "在哪" in player_message or "案发" in player_message:
|
||||
times = [
|
||||
f"{name}回忆道:\"我当时在……让我想想。\"",
|
||||
f"{name}说:\"那段时间我一个人在房间里。\"",
|
||||
f"{name}皱眉:\"我不太确定具体时间,但我确实听到了什么声音。\"",
|
||||
]
|
||||
return random.choice(times)
|
||||
|
||||
if "线索" in player_message or "证据" in player_message or "发现" in player_message:
|
||||
clues_replies = [
|
||||
f"{name}点头:\"这个线索确实值得注意。\"",
|
||||
f"{name}沉思:\"但这个线索也可能是在误导我们。\"",
|
||||
f"{name}说:\"我也有一个发现,但还不确定是否相关。\"",
|
||||
]
|
||||
return random.choice(clues_replies)
|
||||
|
||||
if "知道" in player_message or "秘密" in player_message:
|
||||
secrets = [
|
||||
f"{name}回避了你的目光:\"我什么也不知道。\"",
|
||||
f"{name}沉默了一会儿:\"每个人都有不想说的秘密,不是吗?\"",
|
||||
f"{name}说:\"我只能告诉你,事情不是你看到的那样。\"",
|
||||
]
|
||||
return random.choice(secrets)
|
||||
|
||||
if "动机" in player_message or "为什么" in player_message or "目的" in player_message:
|
||||
motives = [
|
||||
f"{name}说:\"每个人都有自己的理由,但有些理由……\"",
|
||||
f"{name}耸耸肩:\"动机?也许我们需要先搞清楚发生了什么。\"",
|
||||
f"{name}看着你:\"你确定你想知道真正的动机?\"",
|
||||
]
|
||||
return random.choice(motives)
|
||||
|
||||
generic = [
|
||||
f"{name}思考了一下:\"这个嘛,我说不好。\"",
|
||||
f"{name}回答:\"我不太确定,但我觉得我们应该继续调查。\"",
|
||||
f"{name}说:\"你说得有一定道理,但可能还有其他可能性。\"",
|
||||
f"{name}点头:\"有意思,继续说。\"",
|
||||
]
|
||||
return random.choice(generic)
|
||||
Reference in New Issue
Block a user