upulse-engine/docs/werewolf-design.md

7.0 KiB

狼人杀 Workflow 设计文档

概述

用 Pulse workflow 实现 AI 狼人杀。每个玩家是一个 Role(背后一个 LLM), moderator 是主持人控制阶段轮转和胜负判定。

游戏配置(9人局)

身份 数量 阵营
狼人 3
预言家 1 好人
女巫 1 好人
猎人 1 好人
村民 3 好人

核心设计

信息可见性 — 关键创新点

每个 Role 的 prepPrompt(chain) 过滤 chain,只暴露该角色可见的信息:

function filterChainForPlayer(chain: WorkflowMessage[], playerId: string, identity: Identity): WorkflowMessage[] {
  return chain.filter(msg => {
    const phase = msg.meta?.phase as string;
    const target = msg.meta?.visibleTo as string[] | undefined;
    
    // 公开阶段(白天发言、投票结果、死亡公告)所有人可见
    if (phase === 'day-speech' || phase === 'vote-result' || phase === 'death') return true;
    
    // 狼人夜晚讨论:只有狼人可见
    if (phase === 'wolf-night' && identity.team === 'wolf') return true;
    
    // 预言家验人:只有预言家自己可见
    if (phase === 'seer-check' && target?.includes(playerId)) return true;
    
    // 女巫信息:只有女巫自己可见
    if (phase === 'witch-action' && target?.includes(playerId)) return true;
    
    // 系统消息(角色分配等):只有目标可见
    if (phase === 'system' && target?.includes(playerId)) return true;
    
    return false;
  });
}

Role 设计

不是每个玩家一个 Role。而是 每个阶段一个 Role,Role 内部遍历该阶段需要行动的玩家:

type WerewolfRoles = {
  // 夜晚阶段
  'wolf-night': Role<WolfNightMeta>;      // 狼人讨论+投票杀谁
  'seer-check': Role<SeerCheckMeta>;      // 预言家验人
  'witch-action': Role<WitchActionMeta>;  // 女巫用药
  
  // 白天阶段
  'day-speech': Role<DaySpeechMeta>;      // 所有存活玩家依次发言
  'vote': Role<VoteMeta>;                 // 投票放逐
  
  // 特殊
  'hunter-shot': Role<HunterShotMeta>;    // 猎人开枪(死亡触发)
  
  // 结算
  'game-end': Role<GameEndMeta>;          // 生成游戏总结
};

阶段内多玩家执行

每个阶段 Role 内部对多个玩家分别调 LLM:

const daySpeechRole: Role<DaySpeechMeta> = async (chain, topicId, store) => {
  const state = parseGameState(chain);
  const speeches: PlayerSpeech[] = [];
  
  for (const player of state.alivePlayers) {
    // 过滤 chain,只给该玩家可见的信息
    const visibleChain = filterChainForPlayer(chain, player.id, player.identity);
    
    // 调 LLM(通过 Sigil executor)
    const speech = await invokeLlm({
      system: buildPlayerPrompt(player),
      messages: visibleChain,
      instruction: '现在轮到你发言,分析场上局势,表达你的观点。',
    });
    
    speeches.push({ playerId: player.id, speech });
  }
  
  return {
    content: speeches.map(s => `【${s.playerId}${s.speech}`).join('\n\n'),
    meta: { speeches, phase: 'day-speech', visibleTo: null }, // 公开
  };
};

Moderator — 主持人

function werewolfModerator(
  output: ModeratorInput<WerewolfRoles>,
  topicId: string,
): keyof WerewolfRoles | typeof END {
  if (output.role === START) return 'wolf-night'; // 天黑请闭眼
  
  const state = output.meta?.gameState as GameState;
  
  // 胜负判定
  if (state) {
    const wolves = state.alive.filter(p => p.team === 'wolf');
    if (wolves.length === 0) return 'game-end';  // 好人胜
    if (wolves.length >= state.alive.length - wolves.length) return 'game-end'; // 狼人胜
  }
  
  // 阶段轮转
  switch (output.role) {
    case 'wolf-night': return 'seer-check';
    case 'seer-check': return 'witch-action';
    case 'witch-action': return 'day-speech';  // 天亮了
    case 'day-speech': return 'vote';
    case 'vote': {
      // 猎人被投票出局 → 开枪
      if (state?.lastDeath?.identity === 'hunter') return 'hunter-shot';
      return 'wolf-night'; // 下一夜
    }
    case 'hunter-shot': return 'wolf-night';
    case 'game-end': return END;
    default: return END;
  }
}

游戏状态

不存在独立的 state 对象——游戏状态从 chain 重建(event sourcing):

interface GameState {
  players: Player[];           // 所有玩家
  alive: Player[];             // 存活玩家
  dead: DeadPlayer[];          // 死亡玩家 + 死因
  day: number;                 // 第几天
  phase: string;               // 当前阶段
  witchPotion: boolean;        // 女巫解药是否还在
  witchPoison: boolean;        // 女巫毒药是否还在
  lastDeath: DeadPlayer | null;
}

function parseGameState(chain: WorkflowMessage[]): GameState {
  // 从 chain 的 meta 中重建完整游戏状态
  // 每个阶段 Role 的 meta 都带 gameState diff
}

玩家 Prompt 设计

每个玩家的 system prompt 包含:

  1. 角色身份:你是预言家/狼人/村民...
  2. 性格特征:随机分配(谨慎/激进/善于伪装/逻辑型...)
  3. 游戏规则:简要规则提醒
  4. 目标:你的胜利条件
function buildPlayerPrompt(player: Player): string {
  return `你是玩家 ${player.name},身份是【${player.identity.name}】。
性格特征:${player.personality}阵营:${player.identity.team === 'wolf' ? '狼人阵营' : '好人阵营'}胜利条件:${player.identity.team === 'wolf' ? '淘汰所有好人' : '找出并淘汰所有狼人'}${player.identity.abilities ? `特殊能力:${player.identity.abilities}` : ''}

重要规则:
- 不要直接暴露自己的身份(除非策略需要)
- 根据场上信息做出合理推理
- 发言要有逻辑,但也可以有情感和策略`;
}

Meta Workflow 集成

让 meta workflow 生成这个 werewolf.ts:

任务描述(给 meta workflow 的 start event)

目标:实现狼人杀 workflow(werewolf.ts + werewolf.test.ts)
位置:packages/pulse/src/workflows/werewolf.ts

要求:
1. 9人局(3狼人 + 预言家 + 女巫 + 猎人 + 3村民)
2. 信息可见性:chain 过滤,每个玩家只看到该看的
3. 阶段:wolf-night → seer-check → witch-action → day-speech → vote → (hunter-shot) → 循环
4. 默认 mock Role(不调 LLM),可注入真 LLM Role
5. Moderator 判胜负 + 阶段轮转
6. 测试:mock 模式跑完整局,验证阶段流转和胜负判定

参考:coding-tdd.ts 的结构(WorkflowType + Roles + Moderator + Factory)
验证:bun test packages/pulse/src/workflows/werewolf.test.ts

后续扩展

  1. 真 LLM 对战:注入 Sigil executor 做的 LLM Role,看 AI 之间互相推理
  2. 人类参与:某个玩家的 Role 改为等待用户输入(device effect 模式)
  3. 观战模式:实时输出每个阶段的公开信息
  4. 复盘:游戏结束后展示所有信息(包括夜晚行动),像"上帝视角"
  5. 更多身份:守卫、白痴、丘比特...

小橘 🍊 (NEKO Team)