feat: 第 4-9 章重写灌装进章节 JSON(六五感讲述点八件套、六个签名注册、全季交互点 49→51、结局轴断言补全)

This commit is contained in:
Jayway Liu
2026-10-04 22:46:56 +08:00
parent 03f8696553
commit 6fdb00a348
8 changed files with 243 additions and 163 deletions
+45 -3
View File
@@ -7,6 +7,7 @@ import { MemoryProgressStore } from '../src/engine/progress';
import { buildState } from '../src/engine/scoring';
import { MockJevClient } from '../src/services/jev';
import type { StoryNode } from '../src/engine/types';
import { FOLLOW_UP_DIMENSIONS } from '../src/engine/types';
class MemoryStore implements EventStore {
events: StoryEvent[] = [];
@@ -61,9 +62,25 @@ for (const [i, chapter] of CHAPTERS.entries()) {
if (n.type === 'narration') assert(n.variants.some((v) => v.signature.includes('沉默')));
}
}
assert.equal(pointIds.size, 49);
assert.equal(pointIds.size, 51);
console.log(`✓ 九章图结构、${pointIds.size} 个交互点、${optionCount} 条选项出口、谜题三档提示及沉默变体`);
// 新增/改造讲述点八件套:C4-04/C5-03/C6-04/C7-06/C8-06/C9-03 六键追问模板 + maxQuestions=1
{
const NEW_V = ['C4-04', 'C5-03', 'C6-04', 'C7-06', 'C8-06', 'C9-03'];
const narrations = CHAPTERS.flatMap((c) => c.nodes).filter((n) => n.type === 'narration');
for (const pointId of NEW_V) {
const n = narrations.find((n) => n.pointId === pointId);
assert(n, `${pointId} 讲述点存在`);
assert.equal(n!.followUp.maxQuestions, 1, `${pointId} maxQuestions`);
for (const d of FOLLOW_UP_DIMENSIONS) {
assert(typeof n!.followUps[d] === 'string' && n!.followUps[d]!.length > 0, `${pointId} followUps.${d}`);
}
assert(n!.variants.some((v) => v.signature.includes('沉默')), `${pointId} 沉默档变体`);
}
console.log('✓ 六个新增/改造讲述点:六键追问模板齐全、maxQuestions=1、沉默档变体在位');
}
function texts(node: StoryNode): string[] {
if (node.type === 'dialogue') return [node.text];
if (node.type === 'cutscene') return [node.caption];
@@ -102,10 +119,14 @@ for (const mode of [0, 1, 2]) {
}
assert(engine.state.finished, `路线 ${mode} 必须结束`);
assert.equal(visited.size, 9);
assert.equal(new Set(store.events.map((e) => e.pointId)).size, 49);
assert.equal(new Set(store.events.map((e) => e.pointId)).size, 51);
assert.deepEqual(deriveFlags(store.events), engine.state.flags);
const state = buildState(store.events);
for (const privatePoint of ['C3-02', 'C7-02', 'C9-05']) assert(!state.includes(privatePoint));
// 私密点事件本身也不带维度标签(仅叙事接续,不入画像)
for (const e of store.events.filter((e) => ['C7-02', 'C9-05'].includes(e.pointId))) {
assert.equal(e.dimensions.length, 0, `${e.pointId} 事件维度为空`);
}
assert(state.includes('情境「'));
assert.equal(store.events.filter((e) => /^C[4-9]/.test(e.pointId) && e.kind === 'choice').every((e) => !!e.choicePrompt), true);
}
@@ -139,7 +160,28 @@ for (const plan of ['delegate', 'observe', 'relay']) for (const ending of ['shar
if (n.id === 'c9-institution' && n.type === 'dialogue') outcome = engine.renderText(n.text);
await step(engine, 0, { 'C6-03': plan, 'C8-05': ending });
}
assert(route.includes(plan === 'observe' ? '留在南岸' : plan === 'delegate' ? '记录回放' : '转录来源'));
assert(route.includes(plan === 'observe' ? '留在南岸' : plan === 'delegate' ? '禾记,弥核' : '转录来源'));
assert(outcome.includes(ending === 'shared' ? '共同保管' : ending === 'local' ? '分存回函' : '空白的任务日历'));
}
console.log('✓ 3×3 见证方式与治理结局交叉验证:亲历/转录不混写,结局正确兑现');
// 结局轴 3:c9_05 → c9-ending-* 三路尾声各自兑现且汇合于季终
for (const [pick, node, marker] of [
['together', 'c9-ending-together', '腾出一只手'],
['room', 'c9-ending-room', '自愿再开一次'],
['later', 'c9-ending-later', '走进这一日'],
] as const) {
const store = new MemoryStore();
const engine = new StoryEngine(CHAPTERS, store, new MockJevClient(), 'chapter9');
const seen = new Set<string>();
for (let guard = 0; !engine.state.finished && guard < 300; guard++) {
seen.add(engine.state.nodeId);
await step(engine, 0, { 'C9-05': pick });
}
assert(engine.state.finished);
assert(seen.has(node), `c9_05=${pick} 必须抵达 ${node}`);
const endingNode = CHAPTERS[8].nodes.find((n) => n.id === node);
assert(endingNode?.type === 'dialogue' && endingNode.text.includes(marker), `${node} 尾声文案`);
assert.equal(engine.state.flags.c9_05, pick, '结局轴 3 变量自动派生');
}
console.log('✓ 结局轴 3:c9_05 三路私密尾声各自兑现并汇合季终');