Initial commit: 网页联机狼人杀(FastAPI+WebSocket 后端 / React+TS+Sass 前端)

This commit is contained in:
hhhge committed 2026-10-03 20:47:44 +08:00
commit 7f5ea6bee8
55 files changed
+6999

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
"""AI 玩家提示词:身份人设、决策 JSON、公开发言。"""
from __future__ import annotations
import json
RULES = """【狼人杀规则简述】
- 夜晚顺序:守卫守护 → 狼人袭击 → 预言家查验 → 女巫用药(解药/毒药各一瓶,每夜限一瓶,首夜可自救)。
- 守卫不能连续两晚守同一人;「同守同救」(守卫和女巫同时保护被袭者)会导致该玩家死亡。
- 白天:警长竞选(首日)→ 轮流发言 → 全员投票放逐(平票无人出局,警长票计1.5票)。
- 猎人被投票或被狼人杀死时可开枪带走一人(被毒杀不能开枪)。
- 胜负:狼人杀光所有神职或所有村民(屠边)即获胜;好人放逐所有狼人则获胜。"""
KIND_QUESTIONS = {
"kill": ("作为狼人,选择今晚袭击的目标",
"通常优先袭击跳预言家的玩家、发言逻辑威胁大的玩家;也要考虑票型,避开会被怀疑的队友策略。"),
"protect": ("作为守卫,选择今晚守护的玩家",
"通常守护跳预言家的玩家或你认为重要的神职;也可以空守。"),
"check": ("作为预言家,选择今晚查验的玩家",
"优先验身份模糊、立场可疑的玩家,积累信息。"),
"witch_save": ("作为女巫,决定是否使用解药救人",
"如果被袭者是跳预言家的关键角色或你自己(首夜),通常值得救;也要提防狼人骗药。"),
"witch_poison": ("作为女巫,决定是否使用毒药毒杀一人",
"只有在有较强把握某人是狼时才用毒,误毒好人会严重伤害好人阵营;可以不用。"),
"shoot": ("作为猎人,选择开枪带走的目标",
"带走你认为最像狼的玩家;没把握也可以不开枪。"),
"elect": ("决定是否上警竞选警长",
"跳预言家通常应该上警来报查验;狼人可以选择不上警隐藏身份或悍跳预言家。"),
"sheriff_vote": ("投票选出警长", "把票投给你认为能带领好人、发言可信的候选人。"),
"badge": ("作为死亡的警长,决定警徽流向",
"通常把警徽交给最信任的好人(比如你验过的好人);也可以撕毁警徽表达态度。"),
"vote": ("投票放逐一名玩家", "根据发言、逻辑和票型投出你最怀疑的狼人;没把握可以弃票。"),
}
def _state_text(ctx: dict) -> str:
alive = "、".join(f"{p['seat']}号{p['name']}" for p in ctx["alive"])
priv = "\n".join(ctx["private"]) if ctx["private"] else "无"
return (
f"【当前局面】第{ctx['day']}天,{ctx['phase_text']}\n"
f"【存活玩家】{alive}\n"
f"【你的私有信息】\n{priv}\n"
f"【对局记录(公开部分,最近在前为旧)】\n{ctx['log_text'] or '(暂无)'}"
)
def decision_messages(kind: str, ctx: dict) -> tuple[str, str]:
title, tip = KIND_QUESTIONS.get(kind, (kind, ""))
options = ctx["options"]
opts_text = "、".join(str(o) + "号" for o in options) if options else "无"
allow_skip = kind in ("witch_save", "witch_poison", "shoot", "elect", "badge", "vote", "protect", "kill")
system = (
f"你正在玩一局狼人杀。你的名字是「{ctx['name']}」,座位号是{ctx['seat']}号。\n"
f"你的身份:{ctx['role_name']} —— {ctx['role_desc']}\n"
f"你的性格:{ctx['persona'] or '普通玩家'}\n"
f"{RULES}\n"
"你是一个真实的人类玩家。永远不要提及你是AI、模型或程序。"
)
user = (
f"{_state_text(ctx)}\n\n"
f"【你的决策】{title}。\n{tip}\n"
f"可选目标(座位号):{opts_text}\n"
+ ("你可以选择放弃(target 为 null)。\n" if allow_skip else "必须选择一个目标。\n")
+ '只输出 JSON:{"target": 座位号数字或null, "reason": "不超过30字的理由"}'
)
return system, user
def speech_messages(ctx: dict) -> tuple[str, str]:
purpose = ctx.get("purpose") or "day"
if purpose == "election":
task = "现在是警长竞选阶段,请发表你的竞选发言(表决心、报身份或藏身份,符合你的策略)。"
else:
task = "轮到你发言了。请分析当前局势,表达你的身份立场、怀疑对象和投票意向。"
system = (
f"你正在玩一局狼人杀。你的名字是「{ctx['name']}」,座位号是{ctx['seat']}号。\n"
f"你的身份:{ctx['role_name']} —— {ctx['role_desc']}\n"
f"你的性格:{ctx['persona'] or '普通玩家'}\n"
f"{RULES}\n"
"你是一个真实的人类玩家。永远不要提及你是AI、模型或程序。"
"发言要口语化、简短自然,像真人打字,可以引用别人的发言和票型,不要罗列条目。"
)
user = f"{_state_text(ctx)}\n\n【任务】{task}\n直接输出发言内容本身(2~5句话),不要任何前缀、引号或解释。"
return system, user
def options_json(options: list) -> str:
return json.dumps(options, ensure_ascii=False)