在 ~/.upulse/engine/ 中重写狼人杀 workflow(werewolf.ts + werewolf.t

This commit is contained in:
小橘 2026-04-18 10:05:47 +00:00
parent 4974e90c4f
commit 41b831a2a0
2 changed files with 330 additions and 223 deletions

View File

@ -1,7 +1,7 @@
/**
* Werewolf workflow tests mock + +
* Werewolf workflow v3 mock LLM
*
* 🍊 (NEKO Team)
* (NEKO Team)
*/
import { describe, expect, it } from 'bun:test';
@ -22,74 +22,147 @@ import {
createWerewolfWorkflow,
filterChainForPlayer,
parseGameState,
werewolfDefaultMocks,
} from './werewolf.js';
describe('werewolf WorkflowType', () => {
let store: PulseStore;
let tmpDir: string;
describe('游戏初始化', () => {
it('9 人局身份配置:3 狼、预言家、女巫、猎人、3 村民', () => {
const players = createPlayers();
expect(players).toHaveLength(9);
function setup() {
tmpDir = mkdtempSync(join(tmpdir(), 'werewolf-engine-'));
store = createStore({
eventsDbPath: join(tmpDir, 'test.db'),
objectsDir: join(tmpDir, 'objects'),
const roles = players.map((p) => p.identity.name);
expect(roles.filter((r) => r === '狼人')).toHaveLength(3);
expect(roles.filter((r) => r === '村民')).toHaveLength(3);
expect(roles).toContain('预言家');
expect(roles).toContain('女巫');
expect(roles).toContain('猎人');
const teams = { wolf: players.filter((p) => p.identity.team === 'wolf').length, good: 0 };
teams.good = players.length - teams.wolf;
expect(teams.wolf).toBe(3);
expect(teams.good).toBe(6);
expect(players[0]!.id).toBe('p1');
expect(players[8]!.id).toBe('p9');
});
}
function cleanup() {
try {
store?.close();
} catch {
/* ignore */
}
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
}
async function trigger(topicId: string) {
const hash = await store.putObject('werewolf game start');
await store.appendEvent({
occurredAt: Date.now(),
kind: 'werewolf.__start__',
key: topicId,
hash,
meta: JSON.stringify({}),
});
}
it('mock 模式能跑完整局', async () => {
setup();
try {
const wf = createWerewolfWorkflow();
const rule = createWorkflowRule(wf, store);
await trigger('g1');
const order: string[] = [];
for (let i = 0; i < 200; i++) {
const r = await rule.tick();
if (r.executed.length === 0) break;
order.push(...r.executed.map((x) => x.role));
}
expect(order[0]).toBe('wolf-night');
expect(order[order.length - 1]).toBe('__end__');
expect(order[order.length - 2]).toBe('game-end');
expect(order.length).toBeGreaterThanOrEqual(7);
const events = await store.getAfter(0);
expect(events.some((e) => e.kind === 'werewolf.__end__')).toBe(true);
} finally {
cleanup();
}
});
it('阶段正确轮转', () => {
describe('信息可见性过滤', () => {
it('狼人可见狼夜且互通;预言家仅见验人;女巫仅见用药;平民仅见公开阶段', () => {
const players = createPlayers();
const wolf1 = players[0]!;
const wolf2 = players[1]!;
const seer = players[3]!;
const witch = players[4]!;
const villager = players[6]!;
const chain: WorkflowMessage[] = [
{
role: 'wolf-night',
content: '狼人杀人',
meta: { phase: 'wolf-night', targetId: 'p7', visibleTo: ['p1', 'p2', 'p3'] },
timestamp: 1,
},
{
role: 'seer-check',
content: '预言家验人',
meta: {
phase: 'seer-check',
targetId: 'p1',
isWolf: true,
visibleTo: ['p4'],
},
timestamp: 2,
},
{
role: 'witch-action',
content: '女巫行动',
meta: {
phase: 'witch-action',
saved: false,
poisonTarget: null,
visibleTo: ['p5'],
witchPotion: true,
witchPoison: true,
},
timestamp: 3,
},
{
role: 'day-speech',
content: '白天发言',
meta: { phase: 'day-speech', speeches: [] },
timestamp: 4,
},
{
role: 'vote',
content: '投票',
meta: { phase: 'vote', votes: {}, eliminatedId: 'p8' },
timestamp: 5,
},
];
const w1 = filterChainForPlayer(chain, wolf1.id, wolf1.identity);
expect(w1.some((m) => (m.meta as { phase?: string })?.phase === 'wolf-night')).toBe(true);
expect(w1.some((m) => (m.meta as { phase?: string })?.phase === 'seer-check')).toBe(false);
expect(w1.some((m) => (m.meta as { phase?: string })?.phase === 'witch-action')).toBe(false);
const w2 = filterChainForPlayer(chain, wolf2.id, wolf2.identity);
expect(w2.some((m) => (m.meta as { phase?: string })?.phase === 'wolf-night')).toBe(true);
const seerView = filterChainForPlayer(chain, seer.id, seer.identity);
expect(seerView.some((m) => (m.meta as { phase?: string })?.phase === 'seer-check')).toBe(true);
expect(seerView.some((m) => (m.meta as { phase?: string })?.phase === 'wolf-night')).toBe(false);
const witchView = filterChainForPlayer(chain, witch.id, witch.identity);
expect(witchView.some((m) => (m.meta as { phase?: string })?.phase === 'witch-action')).toBe(
true,
);
const villagerView = filterChainForPlayer(chain, villager.id, villager.identity);
expect(
villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'wolf-night'),
).toBe(false);
expect(
villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'seer-check'),
).toBe(false);
expect(
villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'witch-action'),
).toBe(false);
expect(
villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'day-speech'),
).toBe(true);
expect(villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'vote')).toBe(true);
});
it('无 visibleTo 的狼夜 meta 仍对全体狼人可见(兼容)', () => {
const players = createPlayers();
const wolf = players[0]!;
const chain: WorkflowMessage[] = [
{
role: 'wolf-night',
content: 'x',
meta: { phase: 'wolf-night', targetId: 'p7' },
timestamp: 1,
},
];
expect(
filterChainForPlayer(chain, wolf.id, wolf.identity).length,
).toBeGreaterThan(0);
});
});
describe('阶段流转', () => {
it('moderator:黑夜 → 白天 → 投票;终局或猎人后回到狼夜或结束', () => {
const wf = createWerewolfWorkflow();
expect(wf.moderator({ role: START, meta: null }, 'x')).toBe('wolf-night');
expect(
wf.moderator(
{ role: 'wolf-night', meta: { phase: 'wolf-night', targetId: 'p7' } },
{
role: 'wolf-night',
meta: { phase: 'wolf-night', targetId: 'p7', visibleTo: ['p1', 'p2', 'p3'] },
},
'x',
),
).toBe('seer-check');
@ -151,10 +224,31 @@ describe('werewolf WorkflowType', () => {
).toBe('wolf-night');
});
it('狼人全死 → 好人胜', () => {
it('猎人被投票出局后进入 hunter-shot', () => {
const wf = createWerewolfWorkflow();
expect(
wf.moderator(
{
role: 'vote',
meta: {
phase: 'vote',
votes: {},
eliminatedId: 'p6',
gameOver: false,
hunterTriggered: true,
},
},
'x',
),
).toBe('hunter-shot');
});
});
describe('终局判定', () => {
it('狼人全死 → 好人胜局可结束 workflow', () => {
const players = createPlayers();
const wolves = players.filter((p) => p.identity.team === 'wolf');
expect(wolves.length).toBe(3);
expect(wolves).toHaveLength(3);
const aliveWithoutWolves = players.filter((p) => p.identity.team === 'good');
expect(checkGameOver(aliveWithoutWolves)).toBe(true);
@ -186,28 +280,19 @@ describe('werewolf WorkflowType', () => {
).toBe('__end__');
expect(
wf.moderator(
{ role: '__end__', meta: { phase: '__end__' } },
'x',
),
wf.moderator({ role: '__end__', meta: { phase: '__end__' } }, 'x'),
).toBe(END);
});
it('好人数 <= 狼人数 → 狼人胜', () => {
it('好人存活数 ≤ 狼人数 → 狼人胜', () => {
const players = createPlayers();
const alive = [players[0], players[1], players[2], players[6]];
expect(checkGameOver(alive)).toBe(true);
const alive2 = [
players[0],
players[1],
players[2],
players[6],
players[7],
];
const alive2 = [players[0], players[1], players[2], players[6], players[7]];
expect(checkGameOver(alive2)).toBe(true);
const alive3 = [
const balanced = [
players[0],
players[1],
players[2],
@ -216,125 +301,17 @@ describe('werewolf WorkflowType', () => {
players[5],
players[6],
];
expect(checkGameOver(alive3)).toBe(false);
expect(checkGameOver(balanced)).toBe(false);
});
});
it('信息可见性正确', () => {
const players = createPlayers();
const wolf = players[0];
const seer = players[3];
const villager = players[6];
const chain: WorkflowMessage[] = [
{
role: 'wolf-night',
content: '狼人杀人',
meta: { phase: 'wolf-night', targetId: 'p7' },
timestamp: 1,
},
{
role: 'seer-check',
content: '预言家验人',
meta: {
phase: 'seer-check',
targetId: 'p1',
isWolf: true,
visibleTo: ['p4'],
},
timestamp: 2,
},
{
role: 'witch-action',
content: '女巫行动',
meta: {
phase: 'witch-action',
saved: false,
poisonTarget: null,
visibleTo: ['p5'],
},
timestamp: 3,
},
{
role: 'day-speech',
content: '白天发言',
meta: { phase: 'day-speech', speeches: [] },
timestamp: 4,
},
{
role: 'vote',
content: '投票',
meta: { phase: 'vote', votes: {}, eliminatedId: 'p8' },
timestamp: 5,
},
];
const wolfView = filterChainForPlayer(chain, wolf.id, wolf.identity);
expect(wolfView.some((m) => (m.meta as { phase?: string })?.phase === 'wolf-night')).toBe(
true,
);
expect(wolfView.some((m) => (m.meta as { phase?: string })?.phase === 'seer-check')).toBe(
false,
);
expect(wolfView.some((m) => (m.meta as { phase?: string })?.phase === 'witch-action')).toBe(
false,
);
expect(wolfView.some((m) => (m.meta as { phase?: string })?.phase === 'day-speech')).toBe(
true,
);
expect(wolfView.some((m) => (m.meta as { phase?: string })?.phase === 'vote')).toBe(true);
const seerView = filterChainForPlayer(chain, seer.id, seer.identity);
expect(seerView.some((m) => (m.meta as { phase?: string })?.phase === 'seer-check')).toBe(
true,
);
expect(seerView.some((m) => (m.meta as { phase?: string })?.phase === 'wolf-night')).toBe(
false,
);
expect(seerView.some((m) => (m.meta as { phase?: string })?.phase === 'witch-action')).toBe(
false,
);
const villagerView = filterChainForPlayer(chain, villager.id, villager.identity);
expect(
villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'wolf-night'),
).toBe(false);
expect(
villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'seer-check'),
).toBe(false);
expect(
villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'witch-action'),
).toBe(false);
expect(
villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'day-speech'),
).toBe(true);
expect(villagerView.some((m) => (m.meta as { phase?: string })?.phase === 'vote')).toBe(true);
});
it('猎人被投票出局触发开枪', () => {
const wf = createWerewolfWorkflow();
expect(
wf.moderator(
{
role: 'vote',
meta: {
phase: 'vote',
votes: {},
eliminatedId: 'p6',
gameOver: false,
hunterTriggered: true,
},
},
'x',
),
).toBe('hunter-shot');
});
it('女巫用完解药后状态持久化到 chain', () => {
describe('链上状态重建', () => {
it('女巫用完解药后 witchPotion 从 chain 解析一致', () => {
const chain: WorkflowMessage[] = [
{
role: 'wolf-night',
content: '狼人杀 p7',
meta: { phase: 'wolf-night', targetId: 'p7' },
meta: { phase: 'wolf-night', targetId: 'p7', visibleTo: ['p1', 'p2', 'p3'] },
timestamp: 1,
},
{
@ -359,7 +336,7 @@ describe('werewolf WorkflowType', () => {
chain.push({
role: 'wolf-night',
content: '狼人杀 p8',
meta: { phase: 'wolf-night', targetId: 'p8' },
meta: { phase: 'wolf-night', targetId: 'p8', visibleTo: ['p1', 'p2', 'p3'] },
timestamp: 3,
});
@ -368,3 +345,118 @@ describe('werewolf WorkflowType', () => {
expect(state2.lastKill).toBe('p8');
});
});
describe('mock LLM:注入 Role 跑完整 Pulse 局', () => {
let store: PulseStore;
let tmpDir: string;
function setup() {
tmpDir = mkdtempSync(join(tmpdir(), 'werewolf-engine-'));
store = createStore({
eventsDbPath: join(tmpDir, 'test.db'),
objectsDir: join(tmpDir, 'objects'),
});
}
function cleanup() {
try {
store?.close();
} catch {
/* ignore */
}
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
}
async function trigger(topicId: string) {
const hash = await store.putObject('werewolf game start');
await store.appendEvent({
occurredAt: Date.now(),
kind: 'werewolf.__start__',
key: topicId,
hash,
meta: JSON.stringify({}),
});
}
it('默认 mock 能 tick 到 game-end → __end__', async () => {
setup();
try {
const wf = createWerewolfWorkflow();
const rule = createWorkflowRule(wf, store);
await trigger('g1');
const order: string[] = [];
for (let i = 0; i < 200; i++) {
const r = await rule.tick();
if (r.executed.length === 0) break;
order.push(...r.executed.map((x) => x.role));
}
expect(order[0]).toBe('wolf-night');
expect(order[order.length - 1]).toBe('__end__');
expect(order[order.length - 2]).toBe('game-end');
expect(order.length).toBeGreaterThanOrEqual(7);
const events = await store.getAfter(0);
expect(events.some((e) => e.kind === 'werewolf.__end__')).toBe(true);
} finally {
cleanup();
}
});
it('包装 default mocks 模拟「LLM」调用仍跑通全流程', async () => {
setup();
try {
const llmSteps: string[] = [];
const wf = createWerewolfWorkflow({
wolfNightFn: async (chain, topicId, storeArg) => {
llmSteps.push('wolf-night');
return werewolfDefaultMocks.wolfNightFn!(chain, topicId, storeArg);
},
seerCheckFn: async (chain, topicId, storeArg) => {
llmSteps.push('seer-check');
return werewolfDefaultMocks.seerCheckFn!(chain, topicId, storeArg);
},
witchActionFn: async (chain, topicId, storeArg) => {
llmSteps.push('witch-action');
return werewolfDefaultMocks.witchActionFn!(chain, topicId, storeArg);
},
daySpeechFn: async (chain, topicId, storeArg) => {
llmSteps.push('day-speech');
return werewolfDefaultMocks.daySpeechFn!(chain, topicId, storeArg);
},
voteFn: async (chain, topicId, storeArg) => {
llmSteps.push('vote');
return werewolfDefaultMocks.voteFn!(chain, topicId, storeArg);
},
hunterShotFn: async (chain, topicId, storeArg) => {
llmSteps.push('hunter-shot');
return werewolfDefaultMocks.hunterShotFn!(chain, topicId, storeArg);
},
gameEndFn: async (chain, topicId, storeArg) => {
llmSteps.push('game-end');
return werewolfDefaultMocks.gameEndFn!(chain, topicId, storeArg);
},
workflowEndFn: async (chain, topicId, storeArg) => {
llmSteps.push('__end__');
return werewolfDefaultMocks.workflowEndFn!(chain, topicId, storeArg);
},
});
const rule = createWorkflowRule(wf, store);
await trigger('g2');
for (let i = 0; i < 200; i++) {
const r = await rule.tick();
if (r.executed.length === 0) break;
}
expect(llmSteps.length).toBeGreaterThan(0);
expect(llmSteps[0]).toBe('wolf-night');
expect(llmSteps.includes('game-end')).toBe(true);
expect(llmSteps[llmSteps.length - 1]).toBe('__end__');
} finally {
cleanup();
}
});
});

View File

@ -1,12 +1,12 @@
/**
* Werewolf () workflow 9-player game with information asymmetry.
* Werewolf () workflow v3 9 +
*
* Pure roles + START/END automaton. Trigger: werewolf.__start__
* 3 1 1 1 3
* Role
*
* Role chain parseGameState
* mock createWerewolfWorkflow(opts) Role
* Trigger: `werewolf.__start__`
*
* 🍊 (NEKO Team)
* (NEKO Team)
*/
import {
@ -64,6 +64,7 @@ const PERSONALITIES = [
'温和',
] as const;
/** 固定 9 人:p1–p3 狼,p4 预言家,p5 女巫,p6 猎人,p7–p9 村民 */
const IDENTITIES: Identity[] = [
{ name: '狼人', team: 'wolf' },
{ name: '狼人', team: 'wolf' },
@ -201,7 +202,7 @@ export function parseGameState(chain: WorkflowMessage[]): GameState {
};
}
// ── Information visibility filter ──────────────────────────────
// ── Information visibility ───────────────────────────────────────
export function filterChainForPlayer(
chain: WorkflowMessage[],
@ -227,7 +228,10 @@ export function filterChainForPlayer(
return true;
}
if (ph === 'wolf-night') return identity.team === 'wolf';
if (ph === 'wolf-night') {
if (visibleTo && visibleTo.length > 0) return visibleTo.includes(playerId);
return identity.team === 'wolf';
}
if (ph === 'seer-check' || ph === 'witch-action') {
return visibleTo ? visibleTo.includes(playerId) : false;
@ -259,6 +263,8 @@ ${player.identity.abilities ? `特殊能力:${player.identity.abilities}` : ''
export type WolfNightMeta = {
phase: 'wolf-night';
targetId: string;
/** 所有存活狼人 id — 仅狼队可见本场刀法讨论与目标 */
visibleTo: string[];
};
export type SeerCheckMeta = {
@ -303,7 +309,6 @@ export type GameEndMeta = {
summary: string;
};
/** Terminal marker — adapter writes `{name}.__end__` for meta-tester e2e. */
export type WerewolfEndMeta = {
phase: '__end__';
};
@ -321,10 +326,12 @@ export type WerewolfRoles = {
// ── Win condition ──────────────────────────────────────────────
/** 狼人全灭,或好人人数 ≤ 狼人人数(屠边前好人已输) */
export function checkGameOver(alive: Player[]): boolean {
const wolves = alive.filter((p) => p.identity.team === 'wolf');
if (wolves.length === 0) return true;
if (wolves.length >= alive.length - wolves.length) return true;
const good = alive.length - wolves.length;
if (good <= wolves.length) return true;
return false;
}
@ -367,21 +374,30 @@ function werewolfModerator(
}
}
// ── Default mock roles (deterministic, no LLM) ─────────────────
// ── Default mock roles (deterministic) ─────────────────────────
const mockWolfNight: Role<WolfNightMeta> = async (chain) => {
function aliveWolfIds(state: GameState): string[] {
return state.alive
.filter((p) => p.identity.team === 'wolf')
.sort(byPlayerId)
.map((p) => p.id);
}
export const mockWolfNight: Role<WolfNightMeta> = async (chain) => {
const state = parseGameState(chain);
const goods = state.alive
.filter((p) => p.identity.team === 'good')
.sort(byPlayerId);
const goods = state.alive.filter((p) => p.identity.team === 'good').sort(byPlayerId);
const target = goods[0]!;
return {
content: `[狼人夜晚] 狼人决定击杀 ${target.name} (${target.id})`,
meta: { phase: 'wolf-night', targetId: target.id },
meta: {
phase: 'wolf-night',
targetId: target.id,
visibleTo: aliveWolfIds(state),
},
};
};
const mockSeerCheck: Role<SeerCheckMeta> = async (chain) => {
export const mockSeerCheck: Role<SeerCheckMeta> = async (chain) => {
const state = parseGameState(chain);
const seer = state.alive.find((p) => p.identity.name === '预言家');
if (!seer) {
@ -395,9 +411,7 @@ const mockSeerCheck: Role<SeerCheckMeta> = async (chain) => {
},
};
}
const others = state.alive
.filter((p) => p.id !== seer.id)
.sort(byPlayerId);
const others = state.alive.filter((p) => p.id !== seer.id).sort(byPlayerId);
const target = others[0]!;
return {
content: `[预言家查验] ${target.name}${target.identity.team === 'wolf' ? '狼人' : '好人'}`,
@ -410,14 +424,11 @@ const mockSeerCheck: Role<SeerCheckMeta> = async (chain) => {
};
};
const mockWitchAction: Role<WitchActionMeta> = async (chain) => {
export const mockWitchAction: Role<WitchActionMeta> = async (chain) => {
const state = parseGameState(chain);
const witch = state.alive.find((p) => p.identity.name === '女巫');
const saved = false;
/** Mock:每夜毒最小编号存活狼人,加速终局(便于 e2e / meta-tester 在有限 tick 内结束)。 */
const wolves = state.alive
.filter((p) => p.identity.team === 'wolf')
.sort(byPlayerId);
const wolves = state.alive.filter((p) => p.identity.team === 'wolf').sort(byPlayerId);
const poisonTarget =
witch && state.witchPoison && wolves.length > 0 ? wolves[0]!.id : null;
const witchId = witch?.id ?? '';
@ -444,7 +455,7 @@ const mockWitchAction: Role<WitchActionMeta> = async (chain) => {
};
};
const mockDaySpeech: Role<DaySpeechMeta> = async (chain) => {
export const mockDaySpeech: Role<DaySpeechMeta> = async (chain) => {
const state = parseGameState(chain);
const speeches = state.alive.map((p) => {
const visible = filterChainForPlayer(chain, p.id, p.identity);
@ -459,15 +470,11 @@ const mockDaySpeech: Role<DaySpeechMeta> = async (chain) => {
};
};
const mockVote: Role<VoteMeta> = async (chain) => {
export const mockVote: Role<VoteMeta> = async (chain) => {
const state = parseGameState(chain);
const alive = state.alive;
const wolves = alive
.filter((p) => p.identity.team === 'wolf')
.sort(byPlayerId);
/** 狼已全灭时不再放逐(好人胜局已定) */
const eliminated =
wolves.length > 0 ? wolves[0] : undefined;
const wolves = alive.filter((p) => p.identity.team === 'wolf').sort(byPlayerId);
const eliminated = wolves.length > 0 ? wolves[0] : undefined;
const eliminatedId = eliminated?.id ?? null;
const votes: Record<string, string> = {};
@ -475,9 +482,7 @@ const mockVote: Role<VoteMeta> = async (chain) => {
for (const p of alive) votes[p.id] = eliminatedId;
}
const aliveAfter = eliminatedId
? alive.filter((p) => p.id !== eliminatedId)
: alive;
const aliveAfter = eliminatedId ? alive.filter((p) => p.id !== eliminatedId) : alive;
const gameOver = checkGameOver(aliveAfter);
const hunterTriggered = eliminated?.identity.name === '猎人';
@ -493,11 +498,9 @@ const mockVote: Role<VoteMeta> = async (chain) => {
};
};
const mockHunterShot: Role<HunterShotMeta> = async (chain) => {
export const mockHunterShot: Role<HunterShotMeta> = async (chain) => {
const state = parseGameState(chain);
const wolves = state.alive
.filter((p) => p.identity.team === 'wolf')
.sort(byPlayerId);
const wolves = state.alive.filter((p) => p.identity.team === 'wolf').sort(byPlayerId);
const target = wolves[0] ?? state.alive.sort(byPlayerId)[0]!;
const aliveAfter = state.alive.filter((p) => p.id !== target.id);
const gameOver = checkGameOver(aliveAfter);
@ -512,7 +515,7 @@ const mockHunterShot: Role<HunterShotMeta> = async (chain) => {
};
};
const mockGameEnd: Role<GameEndMeta> = async (chain) => {
export const mockGameEnd: Role<GameEndMeta> = async (chain) => {
const state = parseGameState(chain);
const wolves = state.alive.filter((p) => p.identity.team === 'wolf');
const winner: Team = wolves.length === 0 ? 'good' : 'wolf';
@ -526,7 +529,7 @@ const mockGameEnd: Role<GameEndMeta> = async (chain) => {
};
};
const mockWorkflowEnd: Role<WerewolfEndMeta> = async () => ({
export const mockWorkflowEnd: Role<WerewolfEndMeta> = async () => ({
content: '[workflow finished]',
meta: { phase: '__end__' as const },
});
@ -544,6 +547,18 @@ export type CreateWerewolfWorkflowOpts = {
workflowEndFn?: Role<WerewolfEndMeta>;
};
/** 默认 mock 实现,可供测试包装为「LLM」调用 */
export const werewolfDefaultMocks: CreateWerewolfWorkflowOpts = {
wolfNightFn: mockWolfNight,
seerCheckFn: mockSeerCheck,
witchActionFn: mockWitchAction,
daySpeechFn: mockDaySpeech,
voteFn: mockVote,
hunterShotFn: mockHunterShot,
gameEndFn: mockGameEnd,
workflowEndFn: mockWorkflowEnd,
};
export function createWerewolfWorkflow(
opts?: CreateWerewolfWorkflowOpts,
): WorkflowType<WerewolfRoles> {