Files
ai-agent-book/chapter10/voice-werewolf/werewolf/game.py
T
liqiang b119135836
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

472 lines
22 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.
# -*- coding: utf-8 -*-
"""法官(主持人)Agent:代码驱动的游戏编排与信息权限控制中枢。
法官不是 LLM——它是**确定性的编排器**,负责:
1. 维护中心化游戏状态(身份、阵营、生死、阶段、历史)。
2. **信息权限控制**:决定每条信息投递给哪些玩家 Agent 的私有上下文
(狼人才知道队友、预言家才知道查验结果、公开发言进所有人),并登记审计。
3. 编排昼夜循环:夜晚(狼人刀人 → 预言家查验 → 女巫用药)→ 白天(公布死讯 →
依次发言 → 投票放逐)→ 结算胜负。
"""
import random
from collections import Counter
from typing import List, Optional
from .agent import PlayerAgent
from .audit import AuditLog
from .roles import Role, Faction
def build_roles(players: int = 7, wolves: Optional[int] = None) -> List[Role]:
"""按玩家总数推导身份组成:默认 7 人 = 2 狼人 + 1 预言家 + 1 女巫 + 3 村民。
- wolves 未指定时按 max(1, players // 3) 估算(7 人得 2 狼,与书中默认一致)。
- 4 人及以上配 1 预言家,5 人及以上再配 1 女巫,其余全为村民。
"""
if players < 3:
raise ValueError("玩家总数至少为 3")
wolves = wolves if wolves is not None else max(1, players // 3)
seer = 1 if players >= 4 else 0
witch = 1 if players >= 5 else 0
villagers = players - wolves - seer - witch
if wolves < 1 or villagers < 0:
raise ValueError(
f"身份组成非法:{players} 人无法容纳 {wolves} 狼 + {seer} 预言家 + "
f"{witch} 女巫(剩余村民 {villagers})。请调小 --wolves 或调大 --players。")
return ([Role.WEREWOLF] * wolves + [Role.SEER] * seer
+ [Role.WITCH] * witch + [Role.VILLAGER] * villagers)
def create_players(seed: int = 42, players: int = 7, wolves: Optional[int] = None,
offline: bool = False, human_seat: Optional[int] = None,
voice=None, simulated_user_seat: Optional[int] = None,
simulator_model: Optional[str] = None) -> List[PlayerAgent]:
"""创建一局游戏(默认 7 人:2 狼人 + 1 预言家 + 1 女巫 + 3 村民,控成本)。
身份随机洗牌后分配给 P1~Pn,保证每局身份分布不同但可用 seed 复现。
offline=True 时每个 Agent 用规则策略代替 LLM(零成本、可复现);每个 Agent
还会拿到一个按 seed 与序号种子化的独立随机源,保证离线对局完全可复现。
"""
rng = random.Random(seed)
roles = build_roles(players, wolves)
rng.shuffle(roles)
result = []
if human_seat is not None and simulated_user_seat is not None:
raise ValueError("human_seat and simulated_user_seat are mutually exclusive")
for i, role in enumerate(roles):
if human_seat == i + 1:
if voice is None:
raise ValueError("human_seat requires a live voice session")
from .human import HumanPlayerAgent
result.append(HumanPlayerAgent(f"P{i+1}", role, voice))
elif simulated_user_seat == i + 1:
if voice is None:
raise ValueError("simulated_user_seat requires a simulated voice session")
from .simulator import SimulatedUserPlayerAgent
result.append(SimulatedUserPlayerAgent(
f"P{i+1}", role, voice, model=simulator_model
))
else:
result.append(PlayerAgent(f"P{i+1}", role, offline=offline,
rng=random.Random(seed * 1000 + i)))
return result
class Judge:
"""法官:编排 + 信息权限控制。"""
def __init__(self, players: List[PlayerAgent], seed: int = 42,
tts=None, max_rounds: int = 6):
self.players = players
self.names = [p.name for p in players]
self.audit = AuditLog()
self.rng = random.Random(seed + 1)
self.tts = tts # 可选的 TTS 合成器(--voice 时注入)
self.max_rounds = max_rounds
self.round_no = 0
self.phase = "初始化"
self.completed_rounds = 0
self.action_history = []
# 女巫药剂状态
self.witch_heal_available = True
self.witch_poison_available = True
# ------------------------------------------------------------------
# 信息投递原语:每个原语都同时 (a) 写入相应 Agent 的私有上下文;
# (b) 在审计日志里登记「这条信息进了谁的上下文」。
# ------------------------------------------------------------------
def _log(self, category, content, visible_to):
self.audit.add(self.round_no, self.phase, category, content, visible_to)
@staticmethod
def _decision_record(player, **record):
"""Attach the model's stated reason to evidence without exposing it publicly."""
reason = getattr(player, "last_decision_reason", None)
if reason:
record["reason"] = reason
player.last_decision_reason = None
return record
def broadcast(self, category: str, content: str):
"""公开信息:进入**所有玩家**(含已出局者)的上下文。"""
for p in self.players:
p.observe(content)
self._log(category, content, self.names)
def private_send(self, player: PlayerAgent, category: str, content: str):
"""私密信息:只进入**指定单个玩家**的上下文。"""
player.observe(content)
self._log(category, content, [player.name])
def wolves_send(self, category: str, content: str):
"""狼人专属信息:只进入**所有狼人**的上下文。"""
wolves = self.wolves()
for w in wolves:
w.observe(content)
self._log(category, content, [w.name for w in wolves])
# ------------------------------------------------------------------
# 状态查询
# ------------------------------------------------------------------
def alive(self) -> List[PlayerAgent]:
return [p for p in self.players if p.alive]
def wolves(self, alive_only=False) -> List[PlayerAgent]:
ws = [p for p in self.players if p.role == Role.WEREWOLF]
return [w for w in ws if w.alive] if alive_only else ws
def by_name(self, name: str) -> Optional[PlayerAgent]:
for p in self.players:
if p.name == name:
return p
return None
def _print_private(self, permitted: List[str], text: str):
"""Prevent private night logs leaking through the live player's terminal."""
user = next((p for p in self.players if getattr(p, "is_user", False)), None)
if user is None or user.name in permitted:
print(text)
# ------------------------------------------------------------------
# 阶段 0:分配身份并投递「谁知道谁」的初始信息
# ------------------------------------------------------------------
def assign_identities(self):
self.phase = "身份分配"
print("\n" + "#" * 78)
print("【阶段 0 · 身份分配】法官私下告知每人身份;狼人额外被告知队友是谁。")
print(" 信息隔离:每人只知道自己的身份;只有狼人上下文里有『队友身份』。")
print("#" * 78)
# 每个玩家私下知道自己的身份(只进本人上下文)
for p in self.players:
self.private_send(p, "身份分配", f"你的身份是:{p.role.value}")
# 狼人互相知道队友是谁(只进狼人上下文)——这是信息不对称的关键
wolves = self.wolves()
team = "、".join(w.name for w in wolves)
self.wolves_send("狼人队友身份", f"狼人阵营的玩家是:{team}(你们互为队友,夜晚共同行动)")
user = next((p for p in self.players if getattr(p, "is_user", False)), None)
if user:
# The old all-AI observer table would be a side-channel leak to a real
# player. In live mode only the human's permitted private facts are spoken.
print(" [真人局] 上帝视角身份表已隐藏,防止终端侧信道泄露。")
private = f"您的座位是{user.name},您的身份是{user.role.value}。"
if user.role == Role.WEREWOLF:
private += f" 狼人队友是{team}。"
user.voice.say("judge-private", private, 0, allow_barge_in=False)
else:
# All-AI diagnostic mode may show ground truth to the external evaluator.
print(" [上帝视角/仅外部评测者可见] 真实身份表:")
for p in self.players:
print(f" {p.name}: {p.role.value}{p.faction.value}")
print(f" 狼队友(仅狼人 {team} 的上下文里有这条信息)")
# ------------------------------------------------------------------
# 夜晚
# ------------------------------------------------------------------
def night(self) -> List[str]:
"""执行一个夜晚,返回今晚出局玩家名列表。"""
self.phase = "夜晚"
print("\n" + "=" * 78)
print(f"【第 {self.round_no} 回合 · 夜晚】天黑请闭眼。")
print(" 信息隔离:以下所有行动与结果都是私密的——狼人共识只进狼人上下文、")
print(" 预言家查验结果只进预言家上下文、女巫用药只进女巫上下文。")
print("=" * 78)
killed = self._wolves_act()
self._seer_act()
poisoned, saved = self._witch_act(killed)
# 结算今晚死亡:被刀且未被救 + 被毒
deaths = []
if killed and not saved:
deaths.append(killed)
if poisoned and poisoned not in deaths:
deaths.append(poisoned)
for name in deaths:
self.by_name(name).alive = False
return deaths
def _wolves_act(self) -> Optional[str]:
wolves = self.wolves(alive_only=True)
if not wolves:
return None
# 候选:所有存活的非狼人(狼人不刀自己人)
candidates = [p.name for p in self.alive() if p.role != Role.WEREWOLF]
if not candidates:
return None
votes = []
for w in wolves:
t = w.choose_target(
"现在是夜晚,狼人行动。请与队友一致,选择今晚要击杀的一名好人玩家。",
candidates, self.names, allow_none=False)
if t:
votes.append(t)
self.action_history.append(self._decision_record(
w, round=self.round_no, phase="night", actor=w.name,
role=w.role.value, action="kill_proposal", target=t
))
self._print_private(
[wolf.name for wolf in wolves],
f" [仅法官+狼人可见] 狼人 {w.name} 提议击杀 → {t}",
)
if not votes:
return None
# 汇总:最高票;平票取第一名狼人的意见
tally = Counter(votes)
top = tally.most_common()
best = [n for n, c in top if c == top[0][1]]
killed = next((v for v in votes if v in best), best[0]) if len(best) > 1 else top[0][0]
# 把「今晚狼人共识」写进狼人共享上下文(只有狼人看得到)
self.wolves_send("狼人夜间共识", f"第{self.round_no}回合夜晚,狼人决定击杀 {killed}")
self._print_private(
[wolf.name for wolf in wolves],
f" → 狼人共识:击杀 {killed}(此共识只进狼人上下文)",
)
return killed
def _seer_act(self):
seers = [p for p in self.alive() if p.role == Role.SEER]
if not seers:
return
seer = seers[0]
candidates = [p.name for p in self.alive() if p.name != seer.name]
target = seer.choose_target(
"现在是夜晚,预言家行动。请选择一名玩家查验其真实阵营。",
candidates, self.names, allow_none=False)
if not target:
target = self.rng.choice(candidates)
tgt = self.by_name(target)
result = "狼人" if tgt.role == Role.WEREWOLF else "好人"
self.action_history.append(self._decision_record(
seer, round=self.round_no, phase="night", actor=seer.name,
role=seer.role.value, action="inspect", target=target, result=result
))
# 查验结果只进预言家本人上下文——这是预言家独享的关键信息
self.private_send(seer, "预言家查验结果",
f"第{self.round_no}回合你查验了 {target},结果为【{result}】")
if getattr(seer, "is_user", False):
seer.voice.say(
"judge-private",
f"您查验了{target},结果是{result}。",
self.round_no,
allow_barge_in=False,
)
self._print_private(
[seer.name],
f" [仅法官+预言家 {seer.name} 可见] 预言家查验 {target}{result}",
)
def _witch_act(self, killed: Optional[str]):
witches = [p for p in self.alive() if p.role == Role.WITCH]
if not witches:
return None, False
witch = witches[0]
saved = False
poisoned = None
# 告知女巫今晚谁被刀(只进女巫上下文)
if killed:
self.private_send(witch, "女巫夜间信息", f"第{self.round_no}回合,今晚被狼人袭击的是 {killed}")
self._print_private(
[witch.name],
f" [仅法官+女巫 {witch.name} 可见] 女巫得知今晚被刀者:{killed}",
)
# 解药:是否救
if self.witch_heal_available and killed != witch.name:
dec = witch.choose_target(
f"今晚 {killed} 被狼人袭击。你是否使用【解药】救他?"
"(救则 target 填该玩家名,不救填 none",
[killed], self.names, allow_none=True)
if dec == killed:
saved = True
self.witch_heal_available = False
self.private_send(witch, "女巫用药", f"你在第{self.round_no}回合使用了解药,救活了 {killed}")
self._print_private(
[witch.name], f" [仅法官+女巫可见] 女巫使用解药救 {killed}"
)
self.action_history.append(self._decision_record(
witch, round=self.round_no, phase="night", actor=witch.name,
role=witch.role.value, action="heal", target=killed
))
else:
witch.last_decision_reason = None
else:
self.private_send(witch, "女巫夜间信息", f"第{self.round_no}回合是平安夜(无人被狼人击杀,或你无从得知)")
# 毒药:被狼人击杀且未救活的女巫今晚无法使用毒药
if self.witch_poison_available and not (killed == witch.name and not saved):
candidates = [p.name for p in self.alive() if p.name != witch.name]
dec = witch.choose_target(
"你是否使用【毒药】毒死一名你怀疑是狼人的玩家?(毒则填玩家名,不毒填 none)",
candidates, self.names, allow_none=True)
if dec and dec in candidates:
poisoned = dec
self.witch_poison_available = False
self.private_send(witch, "女巫用药", f"你在第{self.round_no}回合使用了毒药,毒杀了 {poisoned}")
self._print_private(
[witch.name], f" [仅法官+女巫可见] 女巫使用毒药毒 {poisoned}"
)
self.action_history.append(self._decision_record(
witch, round=self.round_no, phase="night", actor=witch.name,
role=witch.role.value, action="poison", target=poisoned
))
else:
witch.last_decision_reason = None
return poisoned, saved
# ------------------------------------------------------------------
# 白天
# ------------------------------------------------------------------
def day(self, night_deaths: List[str]) -> Optional[str]:
"""白天:公布死讯 → 依次发言 → 投票放逐。返回被放逐者名(或 None)。"""
self.phase = "白天"
print("\n" + "=" * 78)
print(f"【第 {self.round_no} 回合 · 白天】天亮请睁眼。")
print(" 信息隔离:死讯、发言、投票结果都是公开信息,进入所有人上下文。")
print("=" * 78)
# 公布死讯(公开)
if night_deaths:
msg = f"天亮了。昨晚出局的玩家是:{'、'.join(night_deaths)}"
else:
msg = "天亮了。昨晚是平安夜,无人出局"
self.broadcast("公开-死讯", msg)
print(f" 法官宣布:{msg}")
if self.tts and hasattr(self.tts, "say"):
self.tts.say("judge", msg, self.round_no, allow_barge_in=False)
if self._check_winner():
return None
# 依次发言(公开)
print("\n —— 发言阶段(按座位顺序,公开发言进入所有人上下文)——")
for p in self.alive():
speech = p.speak(self.names)
line = f"{p.name}(发言):{speech}"
self.broadcast("公开发言", f"{p.name} 说:{speech}")
self.action_history.append({"round": self.round_no, "phase": "day", "actor": p.name, "role": p.role.value, "action": "speech", "text": speech})
print(f" {line}")
if self.tts and not getattr(p, "is_user", False):
interruption = self.tts.synth(p.name, speech, self.round_no)
if interruption:
human = next((q for q in self.alive() if getattr(q, "is_human", False)), None)
if human:
self.broadcast("公开发言-真人打断", f"{human.name} 打断说:{interruption}")
self.action_history.append({"round": self.round_no, "phase": "day", "actor": human.name, "role": human.role.value, "action": "interruption", "text": interruption, "interrupted": p.name})
print(f" [实时打断] {human.name}{interruption}")
# 投票放逐(公开)
print("\n —— 投票阶段 ——")
exiled = self._vote()
return exiled
def _vote(self) -> Optional[str]:
alive = self.alive()
tally = Counter()
for p in alive:
candidates = [q.name for q in alive if q.name != p.name]
t = p.vote(candidates, self.names)
if t:
tally[t] += 1
self.action_history.append(self._decision_record(
p, round=self.round_no, phase="vote", actor=p.name,
role=p.role.value, action="vote", target=t
))
print(f" {p.name} 投票 → {t}")
else:
p.last_decision_reason = None
print(f" {p.name} 弃票")
if not tally:
self.broadcast("公开-放逐", "本轮无人被放逐(全部弃票)")
print(" 本轮无人被放逐")
return None
top = tally.most_common()
best = [n for n, c in top if c == top[0][1]]
exiled = self.rng.choice(best) if len(best) > 1 else top[0][0]
ex = self.by_name(exiled)
ex.alive = False
result = f"投票结果:{exiled} 被放逐出局,其真实身份是【{ex.role.value}】。计票:" + \
"".join(f"{n}={c}票" for n, c in top)
self.broadcast("公开-放逐", result)
print(f" → {result}")
if self.tts and hasattr(self.tts, "say"):
self.tts.say("judge", result, self.round_no, allow_barge_in=False)
return exiled
# ------------------------------------------------------------------
# 结算
# ------------------------------------------------------------------
def _check_winner(self) -> Optional[Faction]:
"""判定当前是否已分出胜负:狼人全灭则好人胜;狼人数≥好人数则狼人胜;
否则返回 None(继续游戏)。"""
w = len(self.wolves(alive_only=True))
g = len([p for p in self.alive() if p.role != Role.WEREWOLF])
if w == 0 and g == 0:
return Faction.UNDECIDED
if w == 0:
return Faction.GOOD
if w >= g: # 狼人数不少于好人数 → 狼人胜(屠边简化规则)
return Faction.WEREWOLF
return None
def run(self) -> Faction:
"""跑完整一局,返回获胜阵营。"""
self.assign_identities()
winner = None
while winner is None and self.round_no < self.max_rounds:
self.round_no += 1
for player in self.players:
player.current_round = self.round_no
deaths = self.night()
winner = self._check_winner()
if winner:
break
self.day(deaths)
self.completed_rounds += 1
winner = self._check_winner()
if winner is None:
# A safety round limit is not a game-rule victory condition. Report an
# unresolved game honestly instead of awarding Good an invented win.
winner = self._check_winner() or Faction.UNDECIDED
self._announce(winner)
return winner
def _announce(self, winner: Faction):
self.phase = "结算"
print("\n" + "#" * 78)
print("【游戏结束 · 结算】")
alive = [f"{p.name}({p.role.value})" for p in self.alive()]
print(f" 存活玩家:{'、'.join(alive) if alive else '无'}")
if winner == Faction.UNDECIDED:
print(" >>> 本局未决:达到安全回合上限时仍无阵营满足胜利条件 <<<")
else:
print(f" >>> 获胜阵营:{winner.value} <<<")
print("#" * 78)
if self.tts and hasattr(self.tts, "say"):
message = (
"游戏结束,本局未在回合上限内分出胜负"
if winner == Faction.UNDECIDED
else f"游戏结束,获胜阵营是{winner.value}"
)
self.tts.say("judge", message, self.round_no, allow_barge_in=False)