1.3 路径的回放与复现
同一条选择序列推进两次,走出的状态必须逐位一致。本节用三条证据钉死这件事:确定性序列回放两次摘要相同、每个前缀从零重放等于全程记录、500 条采样序列各自回放两次逐条一致,并说明「回放无隐藏状态」到底在保证什么、为什么它是本卷所有度量的前提。
tag
同一条选择序列推进两次,走出的状态必须逐位一致。本节用三条证据钉死这件事:确定性序列回放两次摘要相同、每个前缀从零重放等于全程记录、500 条采样序列各自回放两次逐条一致,并说明「回放无隐藏状态」到底在保证什么、为什么它是本卷所有度量的前提。
同一条路径可以写成三种编码:人可读的规范串、机器可比的紧凑整数元组、以及只对状态算的 FNV-1a 摘要。本节给出三种编码的构造规则,并用 Python 与 TypeScript 两侧对同一条路径算出逐位一致的结果,说明摘要绑定的是编码串而不是文本。
一次通关可以被拆成三种表示:玩家按下的选择序列、引擎走出的节点序列、以及每一步之后的完整状态轨迹。本节用《雾港夜航》的首选项确定性策略走完一条 25 步路径,逐步骤打印好感度、置位 flag、持有物品与 FNV-1a 状态摘要,并给出节点数等于选择数加一的整数不变量。
《叙事策划与叙事工艺》卷三《叙事体验与路径度量》目录:16 章 48 节,把玩家路径变成可度量的对象——路径的表示与采样、分支覆盖率与缺口、结局分布熵、张力曲线与 MAE、后果可见性、节奏密度、好感度曲线形状、一致性残差,以及反推与演化回归。本页给出分工说明、六档可运行性口径与全书 16 章 48 节完整清单。
《叙事策划与叙事工艺》卷三《叙事体验与路径度量》全 50 篇,也是三卷本的收官卷。卷一回答「一个故事能不能先被写成一份规格」,卷二回答「这份规格能不能被一台机器确定性地执行」,卷三回答「执行出来的那些路径能不能被度量,而且度量得诚实」。全书围绕虚构的三夜悬疑叙事《雾港夜航》与工具链 inkwell 展开,只读卷二交付的真源与内核、不新造字段:config/story_spec.json 的 beats(16 个节拍与整数张力)与 metrics(目标张力曲线、覆盖率目标 850‰、结局熵下限 1500 毫比特、两个 MAE 阈值)是本卷两个新的度量输入。第一部分(第 1–4 章)做路径、覆盖与平衡:一次通关的轨迹怎么编码成可回放的路径、随机/贪心/穷举三种采样策略、分支覆盖率的千分比口径与缺口定位、结局分布熵与分支偏斜、结局可达性的回归、后果可见性与假选择检测、选择密度与节奏;第二部分(第 5–9 章)做曲线、张力与一致性:张力从哪里来(叙事节拍而非机制动力学)、张力与释放的节拍计算、情感曲线的构造与目标曲线 MAE、代理指标的口径声明、叙事节奏与密度、节奏冲突检测、好感度随路径的变化与阈值触达分布、关系曲线形状分类、一致性残差与设定-行为对齐回归、失调告警阈值;第三部分(第 10–12 章)做可视化、模拟与测试:纯 xml.etree 出张力曲线、分支图与路径热力图、模拟玩家的路径策略与它对分布的影响、W 层 Playwright 路径批跑与断言、从测试到决策;第四部分(第 13–16 章)做反推、演化与收官:曲线异常点定位、参数到曲线的反推与其稳健性、规格演化的影响面与指标回归、叙事质量代理指标总表与「指标不能声称什么」,最后把三卷装配成一份规格三种消费的完整链路。按章节提供仿真、逻辑内核与原型;当前试点阶段已有复核记录,完整项目的配置、共享模块和交付验收仍在补齐。全书数字均来自本机实跑:单条确定性路径覆盖对话边 446‰、覆盖场景 9/14、覆盖节拍 11/16、只到达 1/5 结局,而 BFS 口径下 115 条对话边全部可达(1000‰),可达状态 9895423 个;全部指标均为 proxy,只做相对比较。
规格演化后(新增 flag、新增物品、新增好感度变量、新增节点),旧存档必须仍能读。本节实跑兼容性矩阵:四种新规格下旧存档全部读入成功并给出补默认值规则;迁移函数三条判据——缺失字段补默认、多余字段丢弃并报告、类型不符拒绝——逐条在本机实跑验证。
《叙事策划与叙事工艺》卷二《叙事引擎与分支系统》附录 A:把全卷 16 章 48 节散落的对话树节点与选项字段、嵌套数组条件文法与四类效果算子汇成索引表,给出本卷 S/P/W/D 四层共 43 个仿真脚本、31 个内核模块与 37 份产物的实盘清点,并附一段可跑的 TypeScript 对话树格式检查器——它连同三个注入负例证明格式约定不是纸面条文。
浏览器里走完同一条确定性策略路径(总是选第一个可用选项,共 25 个事件),读出终局状态,与 Python 侧产物比对 canonical 串与 FNV-1a 摘要。三侧锚点一致:摘要 4162796975、节点 e_lit、结局 灯守。只对整数状态写逐位一致。
给可玩原型挂一套稳定的 data-* 选择器契约:当前节点、每个选择按钮、四个好感度、已置位 flag、持有物品各有一个可寻址的属性。再用 Playwright 无头打开这页单文件 HTML,断言 DOM 已绘制、按钮数等于当前可用选项数、点击后节点推进。
把 config/story_spec.json 与 core/src/inkwell/engine.ts 一起内联进一个单文件 HTML,产出 web/dist/ne09.html。本节实测产物 41641 字节、script src 0、外部 http(s) 链接 0,给出零外部依赖的自检口径与双击即玩的最小交付形态。
四条必须恒为 0 的残差:钳位(可达状态与转移后都在 [0,100] 内)、初值(每个声明角色的 init 落在自身 range 内)、累积口径(新值恒等于 clamp(旧值 + add),set 不写好感度)、门限可达性(15 条门限均可跨过)。四条残差合计 0;lin 与 hai 有 affinity 声明却不在 state.vars,按「声明未用=报告,用了未声明=FAIL」报告。
好感度的分段在规格里就是一串 [aff, 名, >=, 整数] 阈值门,长在选项 when、节点 line_when、结局 when 三处。本节把 15 条门限全抽出来,再逐条扰动 ±5,在同一采样口径下重算覆盖率与可达结局集合,找出 4 条临界门限;并如实说明为什么放宽 5 点不起作用而收紧 5 点才起作用——基线覆盖率已饱和在 1000‰。B 层只打印 WARN,退出码 0。
好感度是一台受钳位约束的整数状态机:状态 = 节点 + 4 个好感度整数 + 18 个 flag + 9 个物品。本节把《雾港夜航》的完整状态空间 BFS 一遍,实测四个变量的可达值域分别是 yan 21、hui 35、shen 12、mu 16,值域之积 141120,而真正可达的好感度四元组只有 87190——四个变量之间并不独立。P 层用同一批选项断言初值来源、add 累加与钳位。
把分支宽度与深度当两条扰动轴:宽度因子 k 把每个有选项节点的出度乘以 2、3、4,路径数从约 4.50×10 的 11 次方一路涨到 10 的 30 次方量级,而去重跳转覆盖率始终 1000‰ 纹丝不动;深度预算 L 把探索限制在 L 步内,可达覆盖率随 L 上升、在 L=22 触顶。B 层只打印软告警,恒 exit 0。
路径数有两层上界:朴素乘积上界 5 的 31 次方约 4.66×10 的 21 次方,真实结构路径数 450208471680,剪枝后 BFS 可达边 115/115 即 1000‰。本节推导优惠券收集器的采样上界(覆盖 115 条边约 613 条路径),并实测随机与贪心两种策略在 N=100/1000/10000/20000 下的覆盖率收敛曲线。
宽度就是一个节点的出度:同时摆在玩家面前的选项数。本节给 48 个对话节点量出度,实测直方图是 5 个 0 出度、16 个 2 出度、26 个 3 出度、1 个 5 出度,最宽的一步是 d_dq_2 的 5 个选项;再按边距离分 23 层看每层宽度,并把 115 个选项去重成 56 条唯一跳转、47 个不同目标。
5 个结局(灯守 / 离港 / 真相 / 缄默 / 沉没)全部可达,但可达不等于易达。本节为每个结局给出一条真实可回放的到达路径,再用 20000 条均匀随机采样算分布:离港 10074、沉没 5507、缄默 3223、灯守 1018、真相仅 178。并厘清 endings 的 id 与 dialogue 的 ending 两个口径。
汇聚本身不是缺陷——115 条选项去重后只剩 56 条唯一跳转。真正的非法汇流是汇聚之后状态不可区分:两条不同入边到达同一节点后,下游全部条件的求值完全一致,信息被丢弃。本节给出可判定口径 D(M),实测 34 个汇聚节点里 22 个冗余、3 个跨源冗余,并用合成规格负例证明检测器会开火。
死路是一个节点在当前状态下没有任何可用选项、且不是结局节点,玩家就卡死了。本节把静态死路与动态死路分开查:静态 0 个;动态用投影状态上的完整 BFS 与 20000 条采样,实测卡死 0 条。再用三组注入负例证明检测器确实会开火,而不是一个永远返回空的摆设。
把「不可达」拆成两种口径:节点不可达(BFS 从未落到它)与选项不可达(它的 when 在任何可达状态下都不成立)。实测两者都是 0,5 个结局全部可达、孤儿结局 0、分支覆盖率 1000‰。另报告规格层面的一处欠声明:节点 d_lt_2 声明了 line_when 却没有 line_else。