在 ~/.upulse/engine/ 中重写狼人杀 workflow(werewolf.ts + werewolf.t
This commit is contained in:
parent
4974e90c4f
commit
41b831a2a0
@ -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('猎人');
|
||||
|
||||
function cleanup() {
|
||||
try {
|
||||
store?.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
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);
|
||||
|
||||
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({}),
|
||||
});
|
||||
}
|
||||
expect(players[0]!.id).toBe('p1');
|
||||
expect(players[8]!.id).toBe('p9');
|
||||
});
|
||||
});
|
||||
|
||||
it('mock 模式能跑完整局', async () => {
|
||||
setup();
|
||||
try {
|
||||
const wf = createWerewolfWorkflow();
|
||||
const rule = createWorkflowRule(wf, store);
|
||||
await trigger('g1');
|
||||
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 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));
|
||||
}
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
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 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 events = await store.getAfter(0);
|
||||
expect(events.some((e) => e.kind === 'werewolf.__end__')).toBe(true);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
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('阶段正确轮转', () => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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> {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user