216 lines
7.0 KiB
Markdown
216 lines
7.0 KiB
Markdown
# 狼人杀 Workflow 设计文档
|
|
|
|
## 概述
|
|
|
|
用 Pulse workflow 实现 AI 狼人杀。每个玩家是一个 Role(背后一个 LLM),
|
|
moderator 是主持人控制阶段轮转和胜负判定。
|
|
|
|
## 游戏配置(9人局)
|
|
|
|
| 身份 | 数量 | 阵营 |
|
|
|------|------|------|
|
|
| 狼人 | 3 | 狼 |
|
|
| 预言家 | 1 | 好人 |
|
|
| 女巫 | 1 | 好人 |
|
|
| 猎人 | 1 | 好人 |
|
|
| 村民 | 3 | 好人 |
|
|
|
|
## 核心设计
|
|
|
|
### 信息可见性 — 关键创新点
|
|
|
|
每个 Role 的 `prepPrompt(chain)` 过滤 chain,只暴露该角色可见的信息:
|
|
|
|
```typescript
|
|
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 内部遍历该阶段需要行动的玩家:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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 — 主持人
|
|
|
|
```typescript
|
|
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):
|
|
|
|
```typescript
|
|
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. **目标**:你的胜利条件
|
|
|
|
```typescript
|
|
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)
|