让 AI 写一个贪吃蛇游戏,往往很快就能得到可以运行的页面。接着加上得分、加速和暂停,改动看起来也不复杂。
但暂停按钮出现了,游戏就真的暂停了吗?如果只是停止画面渲染,后台仍在移动和判定碰撞,玩家恢复时可能已经输了。如果每次恢复都新建一个定时器,反复操作后,蛇又可能越走越快。
“页面上有这个功能”和“系统满足这项需求”之间,隔着一整套行为约定。AI 加快了实现速度,也让这些未经确认的细节更快进入代码。
我更关注的,是怎样把约定变成可恢复、可检查的依据。下面以贪吃蛇为例,串起需求、设计与验证。
一、让关键约定脱离聊天记录
对话适合探索。需求不明确时,可以通过追问和原型逐步收敛;局部文案调整也可以直接完成。
当功能需要持续修改,单靠聊天记录维护项目状态就容易出问题:早期规则埋在历史消息里,后续要求没有说明哪些约定继续有效,工具读取的代码又可能只覆盖局部。上下文压缩、检索遗漏和相互矛盾的要求,都可能使实现偏离意图。
关键约定应当能够被重新找到:本次解决什么问题,哪些行为必须保持,哪些不在范围内,以及怎样确认完成。执行任务前读取相关内容,并与当前代码核对,才能在换会话或继续迭代时恢复工作。
这也是规格驱动开发(Spec-Driven Development)的起点:让需求、设计和验收依据随着实现一起维护。
二、把“加个暂停”写成行为约定
“游戏支持暂停”至少留下了几个问题:暂停时哪些状态不再变化?还能否改变方向?恢复后是否补算暂停期间的时间?游戏结束后按暂停键会怎样?
EARS(Easy Approach to Requirements Syntax)通过事件、状态和条件组织自然语言需求,帮助我们明确触发条件与系统响应。它可以帮助检查表达,但需求是否符合使用场景,仍需要另外判断。
以单机版为例,可以先作出以下约定:
- P1:当运行中的游戏收到暂停操作时,进入暂停状态。暂停期间,蛇的位置、食物和分数保持不变。
- P2:当处于暂停状态时,忽略方向输入;收到恢复操作后,从当前局面继续,暂停时长不计入后续移动。
- **P3:**当游戏尚未开始或已经结束时,暂停操作不改变游戏状态。
这些约定允许我们判断实现是否正确。例如,P1 约束了游戏状态本身,P2 又明确了输入和时间的处理,避免界面停住但恢复后突然补走多步。
写清楚后也更容易讨论取舍:忽略方向输入是否符合玩家习惯?恢复后立即移动还是等待一个完整间隔?这里选择后者,使恢复时刻成为新的计时起点。重要的是在实现前形成一致判断。
同时,要区分行为要求、实现约束和范围边界。假设这是一个已有游戏:暂停时保持局面是行为要求;沿用现有框架、不新增运行时依赖是本次实现的约束;账号、排行榜和联机功能则不在本次范围内。写明这些内容,可以减少 AI 为了补齐一个功能而顺手重构整个项目、引入无关能力的情况。
三、从行为约定到实现约束
需求确定后,技术设计需要解释怎样保证它成立。
针对暂停功能,可以设计为:游戏状态与画面渲染分离,由一个更新循环推进游戏;暂停时停止推进游戏状态;恢复时重置计时基准,保留原有局面;循环的创建与销毁由同一个模块管理,避免重复启动。
这样,一条用户需求就对应到了明确的实现边界。采用哪种框架或计时 API,可以在这些约束内选择。
如果使用文档组织工作,可以让每份文档各自回答一个问题:
| 文档 | 暂停功能中需要记录的内容 |
|---|---|
requirements.md |
P1~P3,以及恢复后等待完整间隔的约定 |
design.md |
状态转换、单一更新循环、输入处理与计时基准 |
tasks.md |
实现状态转换和输入限制,处理恢复计时,补齐对应验证 |
任务不必一次覆盖整个游戏。“完成暂停与恢复,并验证不会产生重复循环”就是可以独立审查的改动。
实现过程中如果发现设计不可行,应说明冲突和影响,再调整设计或需求。完成后,将实际差异同步到文档。cc-sdd 等工具可以辅助组织这一流程,工具之外仍需要有人确认约定与实现保持一致。
四、让测试有能力发现错误
暂停功能可以从几个层次验证。
先检查状态转换:运行状态可以进入暂停,暂停可以恢复,尚未开始或已经结束时不受影响。再使用可控时钟验证输入与计时。
例如,将移动间隔固定为 200ms,关闭加速,并选择不会发生碰撞的局面:暂停后推进时钟 10 秒,蛇的位置、食物和分数都应不变;期间输入一个正常运行时允许的转向,恢复后的方向也应保持原样。恢复后推进 199ms 不应移动,再推进 1ms 才移动一格。
反复暂停与恢复后,还要检查相同模拟时间内的移动次数,以及是否留下额外的活动计时器或更新循环。只断言“每次回调移动一格”并不够,因为重复循环也可能各自满足这个条件。
最后,在浏览器中实际操作按钮和键盘,核对显示状态与游戏行为。浏览器调度会受到负载和后台节流影响,规则计算适合使用可控时钟,实际交互仍需要单独检查。
这里最容易产生误导的是:让 AI 写完实现,再根据实现生成测试。两者可能共享同一个错误。
例如,我们另行约定加速规则:初始移动间隔为 200ms,每获得 5 分减少 20ms,最低为 80ms。数值仅用于演示。
这里的分数指累计得分,达到一个完整的 5 分档位才加速。假设实现误写为“每 4 分加速一次”,结果会出现以下差异:
| 分数 | 按需求应得到的间隔 | 错误实现的结果 |
|---|---|---|
| 4 | 200ms | 180ms |
| 5 | 180ms | 180ms |
| 8 | 180ms | 160ms |
| 10 | 160ms | 160ms |
只检查 5 分和 10 分,测试仍会通过。检查阈值之前及两个阈值之间,才更容易暴露错误。如果测试直接把错误实现的结果当作预期,则连这些用例也失去作用。
同样,最低间隔也应单独验证:25 分时为 100ms,30 分时达到 80ms,35 分时仍保持 80ms。阈值附近与达到上限、下限后的行为,都容易被只覆盖常规流程的测试遗漏。
因此,验收预期应从需求独立推导。审查测试时,我更关心它能否区分正确与错误的行为,而不只是覆盖了多少行代码。测试仍可能由 AI 辅助生成,关键是预期值有独立依据,并经过审查。
五、规格清晰,也可能做错产品
规格让错误更容易被发现,但一份清晰的错误需求,也可以被 AI 高效地实现。
回到 P2:如果玩家希望暂停时提前调整方向,“忽略方向输入”就可能让操作不符合预期。实现完全符合规格,测试也全部通过,仍然需要通过原型和使用反馈决定是否修改规则。
这涉及两种不同的判断:实现是否符合约定,以及约定是否解决了使用者的问题。前者可以借助测试验证,后者需要业务确认、用户反馈和实际使用结果。
我的取舍是,先确认会影响体验和系统边界的决策,再细化实现。对于仍有争议的行为,将其标为待确认项,通过小范围原型验证,而不是让 AI 把推测扩写成一份看起来完整的规格。
六、需求变化可能触发设计重审
如果继续将游戏扩展为共享场景的多人联机版,暂停就从一个本地状态变成了协作规则。
一名玩家暂停时,其他玩家是否继续?暂停期间是否保留碰撞实体?断线是否等同于暂停?这些问题会影响公平性、服务端状态和恢复流程,原来的 P1~P3 已经不够用了。
在这个设计推演中,需要先重新确认行为约定,再确定谁裁定关键状态,以及如何将输入和结果同步给参与者。采用权威服务器时,暂停等关键操作也需要经过服务端规则判断,客户端展示应与裁定结果保持一致。
WebSocket、网络框架和序列化格式的选择,随后依据通信频率、房间规模、允许延迟和压测结果比较。规格变化可能使旧设计失效,架构决策也需要随之更新。
七、控制规格的维护成本
规格与代码共同演进,也会共同产生维护负担。文档描述旧行为、代码实现新规则时,下一次 AI 修改可能继续扩大偏差。
行为变更的审查应同时检查需求与测试。仍待确认的假设需要标明,已经放弃的决策不应继续作为实现依据;任务开始前,也应核对相关规格与代码是否一致。
一次修改的完成记录,可以对应到 P1~P3,写明实际执行了哪些测试、结果如何、哪些浏览器交互已经检查,以及仍有哪些未验证项。任务列表上的勾选不能代替这些证据;验收不通过时,应先判断是实现偏离约定,还是约定本身需要修改,不能直接把预期改成当前代码的输出。
过程的规模可以按风险调整。局部文案修改通常只需要任务描述和页面检查;权限、支付、数据迁移或跨服务协作,则值得投入更明确的行为约束和失败处理。文档应当帮助减少误解,而不是成为每次小改动都要跨越的门槛。
AI 降低了把想法转成代码的成本,也降低了把未经确认的假设转成代码的成本。规格、实现和验证之间需要保留能够相互核对的依据;熟悉业务、代码与运行时的人,仍要判断交付是否解决了问题,以及这些证据是否足够可信。