3.2 确定性与幂等
确定性是同一输入同一输出,幂等是重复应用结果不变,两者常被混为一谈。本节把状态推进拆成三条可断言判据:函数性、set 类算子幂等、add 故意不幂等,并说明全书那句「同输入确定性违例 0」指的是同一状态到同一后继状态,而不是「重复 add 不累加」。
tag
确定性是同一输入同一输出,幂等是重复应用结果不变,两者常被混为一谈。本节把状态推进拆成三条可断言判据:函数性、set 类算子幂等、add 故意不幂等,并说明全书那句「同输入确定性违例 0」指的是同一状态到同一后继状态,而不是「重复 add 不累加」。
短路是求值器的一条优化纪律:and 左假即停、or 左真即停,但它绝不能改变结果。本节给出短路与不短路两种实现的节点访问步数对照——本规格 57 个位点在 16 个代表状态下 eager 1008 步、短路 992 步——并声明复杂度上界:树内 n 个节点时短路步数恒不超过 n。
把同一份规格上的三处消费串成一次联调:穷举给出完整可达状态数与结局分布,执行让确定性策略 first 走到底并算出整数摘要 4162796975,度量给出覆盖率与张力曲线残差。P 层用 TypeScript 内核重放同一条路径,摘要与 S 层逐位相同,验证引擎与规格同源。
把随机路径当模糊测试的输入:用自实现可播种 PRNG 跑 20000 条路径,断言全部终止于 5 个结局之一、无卡死、无越界。实测分布为离港 10074、沉没 5507、缄默 3223、灯守 1018、真相 178,最稀有的真相仅 178 条。Python 与 TypeScript 两侧对同一批路径的终局状态摘要逐位一致。
属性测试不问「跑一遍对不对」,而问「一条命题是否对所有输入都成立」。本节把第 3 / 6 / 8 章的不变量写成六条属性:好感度恒在 [0,100]、set 类算子幂等、add 故意不幂等、静态死路 0、5 个结局全可达、每状态至少一个可用选项,并给每条属性配一个必须 fail 的注入式负例。
什么必须逐位相同、什么允许不同,本节写成一张容差表:整数状态(好感度、flag、item、节点 id、路径)必须逐位;浮点派生量、对象键序、文本渲染、时间戳允许不同。负例把好感度钳位上限从 100 改成 99,实测 12 / 500 条路径摘要改变,首个分歧点定位到路径 #13 第 20 步 c_fg_1a 的 shen。
同一条确定性策略路径(总是选第一个可用选项,25 个事件)在 Python、Node 内核与浏览器内联三个宿主上推进,终局节点 e_lit、结局 灯守、FNV-1a 摘要 4162796975 三侧逐位相同。本节给出宿主差异清单:整数运算、取模、Math.imul 与大整数、对象键序、Unicode 归一化,逐条说明为何不影响状态摘要。
摘要用 FNV-1a 32 位,只对 canonical 规范串(状态)做,不对台词文本做。本节实测同一批 500 条路径(seed=20261016)在 Python 与 TypeScript 两侧逐位相同 500 / 500,前 3 条摘要为 3976226298、3848604931、1198029437,并与卷一同种子采样逐位相同,证明卷二未另立真源。
四类文本一致性校验一次算齐:漏键、悬空键、变体覆盖、占位符。前两类残差为 0;变体覆盖报出 d_lt_2 欠声明(有 line_when 无 line_else,按规则回落,只报不判失败);占位符残差 0,并给一条负例。本章只做结构一致性,译文质量与字体覆盖需真实业务环境,命令行不可复现。
同一条键在不同状态下要渲染不同文本,这是条件变体;同一句话在不同数量下要选不同形式,这是复数。本节给出 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 状态、派生量、绝对时间不能进存档。
把 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 侧读回产物原样重放,断言规范串与摘要两侧完全一致。
叙事引擎可以被抽象成一个三元组:状态(节点、好感度、flag、物品、路径)、事件(一次选项点击)、转移(纯函数 step)。本节把《雾港夜航》的引擎压到这三个词上,并用一份 S 层脚本打印一条 25 步路径的逐步骤状态轨迹与每一步之后的 FNV-1a 摘要。
闭包检验问三件事:条件引用的名字是否都声明过、是否都在某条可达路径上被写过、每个条件位点是否都在至少一个可达状态上取真。S 层脚本重跑一次完整枚举给出报告:52 个条件位点全部取真过,死条件 0 个;另附敏感性自检,证明检验器对注入的静态与动态缺陷都会报警。
一个选项是假选择,当且仅当它不改变任何状态,且与其他选项在终局可达集上不可区分。判据写死在脚本里,不按结果调参。本节用 S 层脚本对《雾港夜航》的 115 个选项逐个判定:空效果选项 16 个,其中 12 个仍判真后果,真正判为假选择的只有 4 个。
带条件的边是顺序约束:lighthouse_base 到 tide_cave 要先有 knows_tide_rule。本节把 5 条约束折成一张表,做两项判定——环检测(DFS 三色法加 Kahn 拓扑序,实测无环)与「不可达的解锁条件」(钥匙会不会锁在门后,实测 0 条),并输出约束表 CSV。