8.1 好感度的状态机
好感度是一台受钳位约束的整数状态机:状态 = 节点 + 4 个好感度整数 + 18 个 flag + 9 个物品。本节把《雾港夜航》的完整状态空间 BFS 一遍,实测四个变量的可达值域分别是 yan 21、hui 35、shen 12、mu 16,值域之积 141120,而真正可达的好感度四元组只有 87190——四个变量之间并不独立。P 层用同一批选项断言初值来源、add 累加与钳位。
category
好感度是一台受钳位约束的整数状态机:状态 = 节点 + 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。
逐层 BFS 把 9895423 个可达状态一层层展开:给出每一层的新增与累计、峰值 frontier,以及 4 个好感度变量的实际值域。去重键是状态的规范串,层号就是最短步数。本节贴出 32 层完整输出,并给出预算 12000000 与峰值内存 3132.8 MB 的口径。
一个状态等于节点乘以 4 个好感度、18 个 flag、9 个物品。本节先算出朴素上界 48 × 101 的四次方 × 2 的 18 次方 × 2 的 9 次方约 6.70×10 的 17 次方,再用 BFS 完整枚举跑出真实可达的 9895423 个状态,并把两者相差约 11 个数量级的来源拆成好感度值域、flag 与物品位图、变量相关性三层。
对同一串选择序列,Python 与 TypeScript 各自推进一遍,比较 canonical 串与 FNV-1a 32 位摘要。本节实测 500 条路径全部逐位相同,并给出前 3 条摘要值对照表;FNV-1a 的 Math.imul 与 Python 的 & 0xFFFFFFFF 是两侧一致的关键。
随机路径采样只用自实现的 Lehmer MINSTD,种子一固定,回放就必然复现。本节实测同一 (seed=20261016, n=500) 采样两次 500 条选择序列逐条相同,而换到 seed=20261017 后逐条相同数为 0、结局分布也整体改变。
选择序列是玩家按下的一串 choice_id,路径是引擎走出来的 node_id 序列。本节用同一趟推进给出双表示对照,实测 115 条选项去重后只有 56 条唯一跳转,因此选项数并不等于路径形状数,并用 Kahn 拓扑排序证明 48 个对话节点构成一张无环 DAG。
快照是某一刻的完整状态,差量是相邻两快照间真正改变的字段。本节沿一条 25 步的确定性路径输出逐步骤差量表,并点出 7 个 flag 与 1 个物品被写却从未被任何条件读取——这类「只增不减的信息」按声明未用=报告处理,不判 FAIL,只如实报出。
确定性是同一输入同一输出,幂等是重复应用结果不变,两者常被混为一谈。本节把状态推进拆成三条可断言判据:函数性、set 类算子幂等、add 故意不幂等,并说明全书那句「同输入确定性违例 0」指的是同一状态到同一后继状态,而不是「重复 add 不累加」。
效果算子只有四个,每个只写一个命名空间:set 写 flag、add 写好感度、grant 与 revoke 写物品。本节把全规格 138 次写入摊平数一遍,校验写入域越界 0,并点出声明了却一次未用的 revoke;P 层用同一批选项断言命名空间互斥,并以把 add 放大到百万级仍被钳回证明钳位确实生效。
可满足性问的是:一个条件在有限状态域里到底有没有解,会不会恒假。本节用边界值枚举在「节点 × 4 个好感度 × 18 个 flag × 9 个物品」上判定,57 个条件位点里死条件 0 个;并处理一处真实特征——lin 与 hai 声明了好感度却不在状态域里,任何门限都结构性不可达。
短路是求值器的一条优化纪律:and 左假即停、or 左真即停,但它绝不能改变结果。本节给出短路与不短路两种实现的节点访问步数对照——本规格 57 个位点在 16 个代表状态下 eager 1008 步、短路 992 步——并声明复杂度上界:树内 n 个节点时短路步数恒不超过 n。
条件在规格里是嵌套数组写成的表达式树,求值器就是把它递归地读成真假。本节给出六种形态的求值语义,用 Python 与 TypeScript 两条链把 story_spec.json 里 57 个条件位点全部求值一遍,并把声明域与使用域分开数:声明 6 种形态只用 5 种,声明 4 个比较算子只用 >= 一种。
卷二交付给卷三《叙事体验与路径度量》的五类接口:规格段、内核函数、仿真产物、原型产物、判据与容差。本节用自检脚本逐项核对磁盘上的段、函数、产物与阈值,再用 W 层脚本在浏览器里复算 16.2 的锚点摘要,并声明边界——本卷只交结构量,体验度量归卷三。