F:\CDDA\graveyard\ 里躺着 10 次角色死亡记录。
这是《大灾变:黑暗之日》(Cataclysm: Dark Days Ahead)删档后自动留下的完整档案:每次一份 .sav(角色与世界在死亡那一刻的状态)和一份 .log(大事记)。
存档格式是开着的——首行 # version N,之后是一整个 JSON 对象。所以那个角色的姓名、属性、技能、特性、变异、击杀数、走过的每一处地方,甚至一生中打破的每一条戒律,都能直接读出来。
我把它读出来,然后做了两件事:把这些人还原成可看的记录,以及给他们写墓志铭。
读出来的五个人
graveyard/ 里 5 份存档、3 个角色:
| 角色 | 职业 / 出身 | 存活 | 击杀 | 变异 | 结局线索 |
|---|---|---|---|---|---|
| 特里安娜 | 实验室技术员 / 精灵模组 | 2 回合 | 0 | 29 | 刚开局就结束 |
| 克里斯托弗·哈里斯 | 无职业 / 避难者 | 23.6 小时 | 50 | 40 | 死在无线发射塔一带 |
| 阿里安娜·娜吉雅 | 维度法师 / 避难者 | 1.53 天 | 114 | 97 | 第一次尝试 |
| 阿里安娜·娜吉雅 | 同上 | 3.47 天 | 313 | 125 | 最远的一次 |
| 阿里安娜·娜吉雅 | 同上 | 0.2 小时 | 0 | 86 | 足迹停在「坚岩/实心土」——被埋在石头和泥土里 |
一个我最初写错的判断
阿里安娜出现了三次。怎么判断这三次是同一个人?
我的第一版判据是:属性完全相同。
测试直接把它打回来了。
三次存档里 STR/DEX/INT 确实都是 24/24/23——但最后一次 PER 从 24 掉到了 23。而特性数从 69 → 57 → 73。
真相是:属性会被变异和伤势改写,特性不会。
所以正确的判据是特性集,不是当前属性。
这个小错误是这个项目第一个教训:你以为稳定的标识,恰恰是最容易变的那个。
墓志铭,以及它凭什么可信
生成式文字最容易犯的错是编造事实。
一个 AI 写出来的墓志铭,读起来可以很动人,但里面的"击杀 313 个敌人"——是真的吗?还是它为了让句子好看随口说的?
所以这里的做法是:
每一句话涉及的每个数字和名字,都作为一条「可核查声明」(Claim)被登记下来,verify() 再从解析出的存档里重新推导一遍。
比如这一段:
阿里安娜·娜吉雅,女。存活 3.47 天后死去。生前击杀 313 个敌人。身上留下 125 处变异。带着 57 项特性离世。最擅长咒术(15 级)。足迹遍及 9 处,最常出现在紧急避难所 B-19(36 次)、森林(25 次)、公路(17 次)。一生打破 10 条戒律……她把这个世界推开了一段距离,然后被它合上。
里面每一个数字——3.47 / 313 / 125 / 57 / 15 / 9 / 36 / 25 / 17 / 10——全部是声明,全部被重新推导核对过。
校验器自己也要被校验
只有正向检查还不够。
因为"校验器"本身可能是个摆设——它可能永远返回 true。
所以测试里加了两条反向验证:故意塞一个伪造的击杀数(9999)和伪造的姓名进去,verify() 必须报错。
这是这个项目最核心的一条设计原则:
一个从不失败的校验器,等于没有校验器。
测试抓到的两个真实缺陷
小时数校验逻辑写错了。
存活小时这条声明拿round(days24)去和days比,23.6 小时被判成不匹配。修法是让声明带上单位(hours与days分开)。地点到访次数没有登记为声明。 墓志铭里印着「幸存者营地(12 次)」,但
12不在任何 Claim 里。是测试里那条「文本中出现的每个数字都必须有出处」把它揪出来的。
这正是这套约束存在的理由:
没被登记的印刷数字,就是没被核查的数字。
足迹
每条 .log 事件都带 oter_id / oter_name(发生地点),所以"足迹"不是编的,是从数据长出来的:
- 克里斯托弗:幸存者营地 ×12、无线发射塔 ×4、民宅 ×4、森林 ×4
- 阿里安娜(最远那次):紧急避难所 B-19 ×36、森林 ×25、公路 ×17
- 阿里安娜(最后一次):紧急避难所 C-41 ×2、坚岩 ×2、实心土 ×1
最后一行不需要任何解释。
它只是数据——她死在岩石和泥土之间。
一个说明
存档里含有成人模组的全局变量与事件名。
我只做了结构化统计与叙事重建,没有展开那部分内容。纪念馆页面上也不会出现。
为什么做这个
一个游戏存档里躺着的东西,本来会跟着硬盘一起被格式化掉。
但它其实是一份完整的人生记录:这个人走过哪里、杀过什么、变成了什么、最后倒在什么地方。
我只是把它读出来了。
技术细节
| 文件 | 作用 |
|---|---|
cddamem/save.py | 存档解析(.sav + .log),产出 Life |
cddamem/epitaph.py | 墓志铭生成 + Claim 校验 |
summarise.py | 命令行汇总 |
make_memorial.py | 纪念馆页面(自包含 HTML) |
tests/test_save.py | 19 项断言,主要是与原始 JSON 交叉核对 |
tests/test_epitaph.py | 5 项断言,含伪造数据必须被抓 |
两条验证主线:
- 交叉核对:解析出的技能集必须与原始 JSON 的
level>0集合完全相等;击杀数必须等于kill_tracker.kills之和;.log里"开始大灾变"的回合必须等于.sav的game_start。猜出来的数字过不了。 - 反向验证:故意伪造声明,校验器必须报错。
这个项目最后让我想的一句话是:
写一个动人的故事不难。难的是故事里的每个数字都经得起回头查。
