12.2 变体与复数
同一条键在不同状态下要渲染不同文本,这是条件变体;同一句话在不同数量下要选不同形式,这是复数。本节给出 2 个带条件变体节点的覆盖矩阵(1 完整、1 欠声明),并给出 zh-CN 与 en-US 的最小复数规则对照——中文退化为单类,复数只在对照语言上有意义。
tag
同一条键在不同状态下要渲染不同文本,这是条件变体;同一句话在不同数量下要选不同形式,这是复数。本节给出 2 个带条件变体节点的覆盖矩阵(1 完整、1 欠声明),并给出 zh-CN 与 en-US 的最小复数规则对照——中文退化为单类,复数只在对照语言上有意义。
文本是规格的一种渲染产物,而键是文本表与规格之间唯一的连接点。本节从 dialogue 段抽出全部文本键——48 条 line、1 条 else、115 条选项文本,共 164 键——给出键命名规则与漏键 / 悬空键检测,并各给一条负例。只做文本的结构一致性,不评价译文质量。
读档后从存档点继续推进,必须与从头一次走完得到逐位相同的终局状态。本节沿 25 步路径在第 1、5、10、15、20、25 步分别存档读档,终局规范串全部相同;并给出负例:把存档里的好感度篡改成 999,宽松读档钳位到 100 并记录修复,严格读档直接拒绝,检测器抓得到。
存档是状态的规范序序列化:键按字典序、值全是整数、无浮点、无时间戳、无随机。本节把一条 25 步路径的终局状态写成 795 字节的存档 JSON,验证字段与 canonical 串一一对应,并让序列化到反序列化再序列化两次字节完全相同;同时点明临时 UI 状态、派生量、绝对时间不能进存档。
用固定种子采样 20 条路径,逐条把选择序列注入页面,断言终局节点、结局与状态摘要与 Python 侧一致,批跑通过 20/20、覆盖 4 种结局。失败时原样打印选择序列便于重放;S 层落成 JSON,P 层 core/test 回放对照。
把 25 步序列走完后的终局状态读回来断言四件事:终局节点 e_lit、结局 灯守、终局摘要 4162796975(与 Python 与 TS 侧逐位一致)、同一序列跑两遍摘要不变;再截图到 out/shots/ 并给出 PNG 字节数。只对整数状态写逐位一致。
用 Playwright 无头打开 web/dist/ne10.html,靠 data-choice-id 选择器契约注入一条 25 步的完整选择序列真玩一遍:逐步记录节点 id 与当前好感度读数,并断言每一步注入的选项都落在规格该节点的可用集合里,终局摘要 4162796975。
同源的意思是同一份规格在 Python 与 TypeScript 两侧推进同一选择序列,整数状态摘要逐位相同。本节用四种确定性策略各走一条路径:Python 侧落出四条路径的选择序列与 FNV-1a 摘要,TypeScript 侧读回产物原样重放,断言规范串与摘要两侧完全一致。
规格驱动的意思是引擎里没有一条硬编码剧情:可用的选项来自 dialogue 的 choices 加 when 求值,状态推进来自 state 的四个效果算子,终局来自 endings 的 when 判定。本节用一份 TypeScript 模块与四条断言证明这台状态机的全部行为都由 spec 的三段决定。
叙事引擎可以被抽象成一个三元组:状态(节点、好感度、flag、物品、路径)、事件(一次选项点击)、转移(纯函数 step)。本节把《雾港夜航》的引擎压到这三个词上,并用一份 S 层脚本打印一条 25 步路径的逐步骤状态轨迹与每一步之后的 FNV-1a 摘要。
《叙事策划与叙事工艺》卷二《叙事引擎与分支系统》目录:16 章 48 节,把 story_spec.json 变成可执行的对话状态机——条件求值、效果应用、确定性回放、状态空间穷举、可玩原型与跨语言摘要。本页给出分工说明、六档可运行性口径与全书 16 章 48 节完整清单。
《叙事策划与叙事工艺》卷二《叙事引擎与分支系统》全 50 篇。卷一回答「一个故事能不能先被写成一份规格」,卷二回答「这份规格能不能被一台机器可复现地、可断言地、可跨语言地执行」。全书围绕虚构的三夜悬疑叙事《雾港夜航》与工具链 inkwell 展开,唯一真源仍是 config/story_spec.json 的十段字段:meta(三幕)、world(4 派系 stance 矩阵、14 个地点的解锁条件、6 条 lore 的揭示旗标)、characters(6 个角色的目标/恐惧/秘密/好感度/弧光)、beats(16 个节拍与整数张力)、scene_graph(14 个节点与 16 条边,其中 5 条带条件)、dialogue(48 个对话节点、115 个选项、5 个结局)、conditions(嵌套数组文法,6 种形式)、state(18 个 flag、4 个好感度、9 个物品)、endings 与 metrics。第一部分(第 1–4 章)搭引擎内核:状态/事件/转移的三元组、规格驱动的对话状态机、引擎与规格的同源、条件求值器与短路、可满足性判定、效果应用、确定性与 set 类幂等、状态快照与差量、确定性回放与逐位一致;第二部分(第 5–8 章)算分支状态空间:990 万可达状态的 BFS 枚举、不可达节点与孤儿结局、死路检测与汇流合法性、结局可达性、分支宽度与采样上界、组合爆炸的控制、好感度状态机与阈值门扰动、整数不变量;第三部分(第 9–12 章)做可玩原型与持久化:把规格内联进单文件 HTML、选择界面与状态显示、Playwright 注入选择序列真玩、断言终局状态与结局、多路径批跑、存档序列化与读档恢复、快照向前兼容、文本表与键的一致性;第四部分(第 13–16 章)做一致、回归与装配:跨语言摘要与 digest、同源不同宿主、偏差与容差、属性测试与随机路径模糊、规模与性能扫描,最后装配成一份规格三处消费的完整链路并交付卷三。按章节提供仿真、逻辑内核与原型;当前试点阶段已有复核记录,完整项目的配置、共享模块和交付验收仍在补齐。全书数字均来自本机实跑:可达状态 9895423 个,分支覆盖率 1000‰,死路节点 0,跨语言摘要 500/500 一致,同输入确定性违例 0。
《叙事策划与叙事工艺》卷一《叙事结构与可执行规格》附录 A:把全卷 16 章 48 节散落的叙事规格九段字段、嵌套数组条件文法与十一条整数不变量汇成索引表,给出卷内文件命名约定、S/P/W 三层完整复现命令与排错顺序,并附一段 Python 与 TypeScript 逐位一致的跨语言状态摘要,作为「文本是规格的渲染」的可验证落点。
闭包检验问三件事:条件引用的名字是否都声明过、是否都在某条可达路径上被写过、每个条件位点是否都在至少一个可达状态上取真。S 层脚本重跑一次完整枚举给出报告:52 个条件位点全部取真过,死条件 0 个;另附敏感性自检,证明检验器对注入的静态与动态缺陷都会报警。
三类状态(flag 布尔 18 个、好感度整数 4 个、物品 9 个)与四个效果算子的写入域一一对应:set 只写 flag、add 只写好感度、grant 与 revoke 只写物品。S 层脚本逐条校验越域并统计每个变量的被写次数,实测 138 条写入全部落在域内,revoke 一次未被使用。
条件是嵌套数组写成的表达式树:三种叶子(旗标 / 好感度比较 / 物品持有)与三种组合(and / or / not)构成完整文法。本节给出文法表,并用 node --test 逐条验证六种形式的正例与负例。实测全规格 58 个条件节点,实际用到 5 种形式——or 声明了却一次没用。
宽度是一个节点同时可选的选项数,深度是从起点到终局走过的边数。本节用 S 层脚本量出《雾港夜航》的平均宽度 2.67、最大宽度 5,并用两条口径量深度:全部 450208471680 条结构路径的长度只取 22/25/28/31 四个值,而采样 20000 次的均值是 25.41 步。
分支拓扑把对话图读成三种原语:分叉是一出多、汇聚是多入一、并行是两条互不可达的去向。本节用 S 层脚本对 48 个节点统计出度与入度,找出全部 9 个分叉点与 8 个汇聚点,再用支配点算法分离出 4 个「所有路径必经」的强制汇流点,最后落出一张 48 行的拓扑表。
把带条件的边当作「可能通」后,场景图上的 BFS 给出结构可达性:14 个场景全部可达,但 5 个场景(counting_house、watch_post、tide_cave、harbor_office、drowned_bell)的入口边全部带条件,可达性完全取决于状态。本节给出入度、出度、最少跳数与最早出现幕四列。
乐观上界只说明「够得到」,不等于「走得到」。本节真的跑两万条采样路径,逐条记录六个角色的好感度峰值,看它是否越过 stages 对应的三个门限。结果是 15/18 可达、3 条不可达(全部落在林与孩身上);更耐读的是「可达」内部的落差——砚的 trust 只有 4‰ 的路径走到,沈的有 112‰。