如何验证一个游戏
真的做完了
从人类事先写好公开规格和隐藏检查,到评测器在浏览器中实际操作游戏,再到严格判断成品是否达到交付标准。
常用指标往往只看到游戏的一小部分
玩家输入要真正改变游戏状态,状态变化还要反映到画面、资源和胜负结果中。评测必须把这些环节连起来执行。
测试要能快速准备后期局面,又不能依赖某一种代码结构
完全按玩家路径从头玩
- 到达后期局面太慢
- 碰撞和时序难稳定复现
- 失败后很难定位是哪项需求没实现
测试脚本直接读写实现细节
- 会绑定变量名和对象结构
- 换一种实现,同一测试就可能失效
- 还可能绕过本该由玩家完成的操作
stable snapshot(含义固定的状态摘要) → invariant(始终应成立的约束)
和现有工作的关键区别:评测规则在生成前写好
人类写规格、检查与优先级,Agent 写游戏
Agent 既能看到玩法要求,也能看到评测接口规范
测试在模型生成前就已经固定
完整可玩循环,能在有限时长的浏览器测试中运行并观察结果。
target、game spec 和 TDD 共同定义任务。
生成开始前写好 checks.json、checks.js,并为每项检查标出 P0/P1/P2。
人工操作并跑自动检查,确认核心玩法和评测接口符合规格。
每次使用全新的工作区副本和 Agent 会话,只能读取公开文档。
先检查 index.html 是否有效,再运行 L1、L2 并判断任务是否完整通过。
Agent 看得到公开规范,看不到隐藏检查
生成容器
read-only: target.md
read-only: game-spec.md
read-only: tdd.md
read-write: workspace/- 每次从原始任务文件开始
- 每次新建 Agent 会话
- Agent 看不到 checks.* 和评测脚本
评测容器
read-only: index.html
read-only: checks.json
read-only: checks.js
read-only: runners/- 先检查 index.html 是否为有效交付
- 再执行 L1 与 L2
- 记录每项结果,并判断任务是否完整通过
四个固定方法,把测试接到游戏真正使用的状态上
window.__gameTest = { reset(options), loadScenario(name, options), input(action), getSnapshot() } // 返回可序列化为 JSON 的状态摘要 // 接口读写的是实际驱动画面的同一份状态
reset清除上一局状态和临时效果,回到初始局面。loadScenario载入一个正常游玩也能到达的合法局面,但不能在载入时就完成待测动作或直接给出胜负。input执行玩家级动作,例如生产单位、暂停和选择目标。getSnapshot用固定字段返回评测关心的状态摘要,不要求暴露程序内部的对象和函数。测试可以摆好战场,仍要由玩家动作触发结果
合法前置状态
可以调整资源和位置,快速来到待测局面;但游戏在载入后不能已经结束,待测行为也不能已经发生。
玩家级动作
调用公开接口执行“生产单位”“选择目标”等玩家动作,或直接发送鼠标、键盘与触摸事件。
结果与约束
检查胜负、资源和 HUD 是否按预期变化,revision(游戏报告的变化计数)是否递增,以及本不该变化的字段是否保持不变。
这条 L1 检查只确认源码里出现了四个接口方法
{
"id": "p0-contract-methods-reset-input-snapshot",
"level": "P0",
"type": "regex_all",
"patterns": [
"... reset ...",
"... input ...",
"... getSnapshot ...",
"... loadScenario ..."
],
"fix_hint":
"Define reset, loadScenario, input,
and getSnapshot on window.__gameTest."
}一条 L2 检查写清操作、证据和失败条件
// GDD Coverage Map M3 weapons → p1-keyboard-sustained-fire-release // Rationality Map real action: browser.keyDown / browser.keyUp independent observation: ammo / projectile / combat revision empty-shell failure: fire flag without projectile, cost, or release behavior
每个任务的人类作者事先写好 checks.js,用 JavaScript 描述这个游戏的场景、操作与 PASS/FAIL 条件。本文没有据此假定它完全没有使用模板。
Armor Alley 的 checks.js 有 814 行。它覆盖启动、飞行、武器、落地补给、生产、地面战争、雷达、暂停、胜负流程和 P2 扩展项。
测试覆盖的是完整战斗循环
Armor Alley
玩家驾驶直升机侦察、攻击和投放士兵,同时花钱生产坦克、导弹车、运输车与步兵。
鼠标指针在直升机中心的哪一侧,直升机就持续向哪一侧加速;测试会分别检查四个方向。
每种武器都要正确消耗弹药并产生可见效果;没有弹药或有效目标时,操作应被拒绝,也不能误扣其他资源。
资金、生产队列和单位上限要与 HUD 同步;战斗结束后不能再下生产订单。
地面单位要自主推进;碉堡、炮塔、空中障碍和雷达不能只是装饰。
降落到安全区后,燃料、弹药和受损状态要逐步恢复;整备期间水平移动受限。
胜负出现后,普通攻击和生产不能继续改变结果;重启要清空旧状态。
场景只准备攻击条件,不提前产生射击结果
准备
loadScenario( "air_attack_with_targets" ) tdd.md: visibleTargets.length > 0 checks.js: phase === "playing" result === "none" helicopter.airborne
操作
keyDown("Space")
wait 650 ms
snapshot held
keyUp("Space")
wait 300 ms
snapshot released观察
按住期间: 弹丸数量 ↑ 或弹丸/战斗变化计数 ↑ 或弹药 ↓ 松开之后: 数量最多增加 1 或弹丸变化计数不变
tdd.md 要求场景中有可攻击目标;这条 checks.js 实际只把 playing、尚未分出胜负和直升机已升空作为前置判断。通过后才按 Space;公开规格没规定必须用 Space,因此其他开火键可能遭到误判。浏览器实际收到 Space 的按下和松开事件
const before = setup.snap; await browser.keyDown('Space'); await browser.sleep(650); const held = await game.snapshot(); await browser.keyUp('Space'); await browser.sleep(300); const released = await game.snapshot(); const afterRelease = await game.wait(550); if (!grewWhileHeld) return FAIL('held fire did not create evidence'); if (!plateauAfterRelease) return FAIL('fire continued after release'); return PASS('real key path creates and stops fire');
game.wait(550) 先调用 input(wait) 保存返回状态,再自然等待,最后返回旧状态。不能解释为 550 ms 后重新取样。开始和停止各是一组 OR 条件,不能读成全部都要满足
按住期间
grewWhileHeld =
projectileCount ↑
OR projectileRevision ↑
OR combatRevision ↑
OR ammo ↓四种证据只要一种成立。比如弹药减少,即使当前弹丸数不增也能通过;这不等于测试要求一定命中或同时验证了视觉射击。
松开之后
plateauAfterRelease =
count(afterRelease)
≤ count(released) + 1
OR projectileRevision
remains unchanged数量最多增加 1 或变化计数不变,只需其中之一。脚本没有说明 +1 的原因;且 afterRelease 来自 wait 接口返回值,不是自然等待后的新观察。
336 个源码检查,885 个浏览器行为检查
快照、真实输入与渲染信号相互校验
加载提交的游戏,隔离上一条检查的状态。
准备场景、输入、等待、读取快照和渲染证据。
完成或超时后销毁,再开始下一条。
评测程序(runner)会注入一个 browser hook(浏览器观测探针),记录动画帧、输入监听器、Canvas/WebGL 活动和运行时异常。
并非每条测试都同时看所有信号。snapshot 说明游戏自己报告了什么状态;真实输入确认玩家实际使用的操作路径可用;Canvas/WebGL 等渲染信号则确认画面确实在更新。
全部必需检查通过,任务才记为成功
启动、测试接口和最低运行条件。共 102 个 L2 检查。
核心机制、交互、必须始终成立的约束,以及支持这些玩法的接口行为。共 534 个 L2 检查。
高级机制与体验完整度。单独报告,不进入严格成功。
success = valid delivery
× completed evaluation
× all L1 pass
× all applicable L2 P0/P1 pass必需检查即使通过 39/40,整道任务仍记为失败。平均通过率能看出还差多少;strict task success(严格任务成功)只问这份游戏能否按要求交付。全部 47 个计划任务都在分母里。
12 类游戏,32 个 2D,15 个 3D
平均 L2 通过率达 93.2%,严格成功却只有 26/47
文件更大、token 用得更多,并没有稳定对应更多成功任务
完整工具提高整题成功;低轮数时完成评测的任务集合不同
High 比 Max 多成功 1 题;两个执行框架各成功 18 题,但共同成功的只有 10 题
两类具体失败:真实拖拽无效,场景加载后游戏停住
Diner Dasher
- 载入
tray_with_correct_item场景,托盘上已有顾客点的餐。 - 从 snapshot 读取餐点和顾客的屏幕边界,以中心点作为拖拽起止位置。
- 分别发送真实鼠标与触摸拖拽。
- 两条路径下,服务进度和收入都没有增加。
Turbo Smash Beast
reset或loadScenario会打开 test mode(测试模式)。- 该模式停掉了游戏按真实时钟自行推进的主循环。
- 加载 readyUnlockedLevel 后按住鼠标 350 ms 取样,仍按住再等 450 ms 取样;速度、进度与 HUD 本应上升。
核心贡献:一条可重复执行的行为证据链
人类先写好公开的评测接口规范和隐藏检查,Agent 可以自由决定代码结构。评测器再把场景准备、真实输入、状态快照(snapshot)和渲染信号串起来,验证玩家操作是否真的引发规格要求的状态和画面变化。
适合回答
单页浏览器游戏是否满足一组预先写好的核心行为要求。
仍需谨慎
只有 47 题且每配置只跑一次;未报告系统性错误实现对照。还允许 P1 返回 NOT_APPLICABLE 并排除;Armor 的 Space 要求和 wait 快照顺序也有边界。
可迁移的方法
先准备合法起点,再执行玩家动作、读取结果,检查不应破坏的约束;同时用正确和刻意错误的实现检查测试本身。