幕后制作
一位开发者,一个 AI 编程助手,以及大量由机器检查的规则。
作者:Nick
Prison Life 是我和 Claude Code(Anthropic 的编程代理)一起写的。游戏是什么、选哪条路线,由我来定;大部分敲代码的活由助手完成。因为除了我和一个负责评审的 AI 代理,没有别人会看这些代码,所以我依靠自动化:规则靠自动检查和构建来强制执行,而不是靠自觉。
任务委派
Claude Code 的主会话负责规划工作,并维护任务跟踪器、设计选择的书面记录和变更历史。它自己不写代码:每一项改动,哪怕再小,都会交给专门的 AI 代理。每个代理都有一份书面定义,规定它的角色和所用的模型。
| 角色 | 模型档位 | 职责 |
|---|---|---|
| 架构 | Opus | 数据模型、tick、线程、寻路、决策 |
| 模拟代码 | Opus | 模拟系统、角色 AI、热点循环 |
| 性能 | Opus | 性能分析、基准测试回退 |
| 评审(只读) | Opus | 评审代码差异;不能编辑文件 |
| 客户端与工具 | Sonnet | Godot 客户端、界面、工具 |
| 测试 | Sonnet | 根据验收标准编写测试 |
| 内容数据 | Sonnet | 依据模式定义生成 JSON 数据 |
| 搜索、运行检查 | Haiku | 代码搜索;运行检查并总结失败原因 |
难的活交给最强的模型,常规的活交给更便宜的模型。如果较便宜的 AI 代理两次没通过检查,任务就升一档;如果最高一档也两次失败,就会停下来问我。
会说“不”的钩子
Claude Code 的钩子(hook)是在助手操作前后运行的脚本。我的钩子强制执行三条规则:
- 红灯不许收工。当会话或 AI 代理在改过代码之后想要结束时,钩子会运行快速检查(构建加快速测试)。如果失败,就拒绝结束。
- 主会话不能编辑代码。钩子会拒绝主会话对源码、测试、内容和工具的写入。
- 评审者是只读的。它的编辑工具已被移除,钩子还会拒绝会修改文件或项目历史的命令。
这些防的是失误,不是蓄意绕过:有心人靠一点 shell 技巧就能绕开;而且其中一个钩子能看到 AI 代理里的红灯,却拦不住它(主会话的停止钩子仍会把它抓出来)。我宁愿把这一点直说。
书面决策与检查
- 每一个不简单的选择都会写下来。任务、里程碑和设计决策以纯文本的形式,和代码放在一起,由任务跟踪器管理。当我们在影响多个任务的方案之间做选择时,选择会连同背景和后果一起记录。
- 检查在我自己的机器上运行。没有云端 CI。一个本地脚本有四种模式:
fast、test、bench和full。脚本和钩子在 Windows 和 macOS 上都是 PowerShell 7。 - 每次改动都要检查。本地自动化会拒绝描述不清或格式不对的改动。对于代码改动,它还会运行快速检查;只改了笔记或网站的改动则跳过构建。
- 基准文件受保护。固定下来的确定性哈希、黄金存档文件和基准测试基线,只有经过我明确批准才会改变,绝不会悄悄变动。
- 项目本身才是事实来源。本地的记忆工具会为项目和过去的对话建立搜索索引,但任何重要的内容同时也会是一个任务、一条记录在案的决策或一份文档。
确定性核心
模拟存在于一个不含任何 Godot 引用的纯 .NET 库中;Godot 客户端只读取它的状态并发送命令。核心遵循固定的规则:世界只通过命令改变,时间是整数 tick,随机性只来自世界的带种子生成器。相同的种子和相同的命令日志,必须得到相同的世界。这些规则由编译器和测试来检查,而不只靠评审;具体怎么做,见开发日志 #1,世界、存档与回放见开发日志 #2。
- 命令是唯一的入口。每条命令都有稳定的类型 ID 和二进制编码,每一条命令都会连同结果一起记入日志,被拒绝的也不例外。
- Tick 有三个按固定顺序执行的阶段。各系统的顺序在代码中的一处设定,改动它就会改变所有哈希。
- 随机性使用我们自己的生成器(SplitMix64 和 xoshiro256**)。具名的随机流由世界种子派生,并保存在世界状态中。对于与顺序无关的工作,则用一个由域、键和 tick 驱动的无状态带键生成器。
- 状态哈希是一个对顺序敏感的 64 位哈希,用于确定性检查。它不是加密哈希。
- 世界是由 1×1 米的瓦片组成的网格,分布在离散的楼层上;墙、门和地板只能通过建造命令来改变。
- 存档与回放共用同一种带校验的文件容器。回放就是种子加命令日志,中途带有校验哈希,播放时会逐步检查。
- 基准测试来自一个无头的场景运行器,带有按机器区分的基线,以及“每个 tick 零内存分配”的门槛。
设计目标是在每秒 20 个 tick 下容纳最多 1000 名囚犯和狱警。这只是目标:现在还没有角色,所以基准测试运行器虽然存在,但在这个规模上还没有测过任何数据。