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

+127
View File
@@ -0,0 +1,127 @@
"""启发式兜底 AI:无 LLM 配置(或 LLM 调用失败)时保证游戏可玩。"""
from __future__ import annotations
import random
class HeuristicAgent:
def __init__(self, player) -> None:
self.player = player
# ------------------------------------------------------------------
async def decide(self, kind: str, ctx: dict):
handler = getattr(self, f"_decide_{kind}", None)
if handler is None:
return None
return handler(ctx)
async def speak(self, ctx: dict) -> str:
return self._speak(ctx)
# ------------------------------------------------------------------ 内部
def _alive_others(self, ctx: dict) -> list[int]:
me = ctx["seat"]
return [p["seat"] for p in ctx["alive"] if p["seat"] != me]
def _claim_targets(self, ctx: dict) -> list[int]:
claimed = [s for s in ctx.get("claimed_seers", [])]
me = self.player
mates = set(ctx.get("teammates", []))
return [s for s in claimed
if s != me.seat and s not in mates]
# ------------------------------------------------------------------ 决策
def _decide_kill(self, ctx: dict):
victims = [o for o in ctx["options"]]
claims = self._claim_targets(ctx)
pool = [s for s in claims if s in victims]
if pool:
return random.choice(pool)
return random.choice(victims) if victims else None
def _decide_protect(self, ctx: dict):
claims = [s for s in self._claim_targets(ctx) if s in ctx["options"]]
if claims:
return random.choice(claims)
return random.choice(ctx["options"]) if ctx["options"] else None
def _decide_check(self, ctx: dict):
return random.choice(ctx["options"]) if ctx["options"] else None
def _decide_witch_save(self, ctx: dict):
target = ctx["options"][0] if ctx["options"] else None
if target is None:
return None
if target == ctx["seat"]:
return target # 首夜自救
if target in ctx.get("claimed_seers", []):
return target
return None # 留药
def _decide_witch_poison(self, ctx: dict):
return None # 保守:不毒
def _decide_shoot(self, ctx: dict):
return random.choice(ctx["options"]) if ctx["options"] else None
def _decide_elect(self, ctx: dict):
if ctx.get("role") == "seer":
return 1
if ctx.get("role") == "werewolf":
return 1 if random.random() < 0.3 else None
return 1 if random.random() < 0.15 else None
def _decide_sheriff_vote(self, ctx: dict):
return random.choice(ctx["options"]) if ctx["options"] else None
def _decide_badge(self, ctx: dict):
claims = [s for s in ctx.get("claimed_seers", []) if s in ctx["options"]]
if claims:
return random.choice(claims)
return random.choice(ctx["options"]) if ctx["options"] else None
def _decide_vote(self, ctx: dict):
claims = [s for s in self._claim_targets(ctx) if s in ctx["options"]]
if claims and ctx.get("role") != "werewolf":
# 好人跟跳预言家的对跳/划水者走:简单起见投被多疑的目标
pass
pool = [o for o in ctx["options"] if o != ctx["seat"]]
if not pool:
return None
if ctx.get("role") == "werewolf":
mates = set(ctx.get("teammates", []))
pool = [o for o in pool if o not in mates] or pool
return random.choice(pool)
# ------------------------------------------------------------------ 发言
def _speak(self, ctx: dict) -> str:
role = ctx.get("role")
day = ctx.get("day", 1)
purpose = ctx.get("purpose")
seat = ctx["seat"]
alive = [p["seat"] for p in ctx["alive"] if p["seat"] != seat]
pick = random.choice(alive) if alive else None
if purpose == "election":
if role == "seer":
return "我是预言家,想拿警徽带队报查验,请大家支持我。"
return f"我{seat}号想为团队做点贡献,上警听听大家的发言。"
if role == "seer":
results = ";".join(
f"{r['seat']}号是{r['result']}" for r in ctx.get("seer_results", [])[-2:]) or "暂时没有信息"
return f"我是预言家,我的查验:{results}。请好人记住我的票。"
if role == "werewolf":
lines = [
f"我是个好人,昨天{pick}号的发言我觉得有点飘,大家可以听听。",
"我没有太多信息,先听后置位怎么说。",
f"这局信息很少,我倾向于{pick}号有问题,先投为敬。",
]
return random.choice(lines)
if role == "witch":
return "我的身份信息暂时不方便透露,从发言上看我觉得节奏有点乱,大家稳住。"
if role == "hunter":
return f"我是普通村民视角,{pick}号的发言我听不太懂,保留怀疑。"
if role == "guard":
return "我从守卫……呃,从好人视角看,这局要小心悍跳,先听大家的。"
if day == 1:
return f"我是村民,没有什么信息,{pick}号的发言给我感觉还行,先观察。"
return f"听到现在,我觉得{pick}号的发言前后有点矛盾,今天我可能跟票他。"