plan 2026-06-18-001 U3 迭代3-4轮(据快走查根因)。
根因(全budget+stage2 都不解,3轮prompt不解):便宜/强模型均**顽固把 tap 命中门控在分离 state 实体/计时器/turn 上**
→ 盲点驱动点不中 → tap-targets 类系统性 E_live/F_wiring/G_input/H 挂。即「让模型写 tap-handler」这条路便宜模型走不通。
引擎线根治(gd-runtime.js):新增 **clickable 声明式基元** `{kind:'clickable',score?,mark?:{shape,color,r},hitW?,hitH?}`——
运行时内置:本帧点中带此组件且 !occupied 的实体 → 自动翻 occupied+spawn 标记(可见)+计分+fx。**离散点击类声明它即可、免写 handler**。
验证:gd-runtime 单测 16(含 clickable 派发) + 全套 60/60;**手写纯声明式(behaviors:[])clickable tap-targets gamedef 过 harness 9/9**(E_live distinct=8/F_wiring/G_input/H 全过)——证基元+管线正确。
GAMEDEF_SYSTEM:教 clickable(替「写 tap-handler」)+ 参照2 改纯声明式 clickable few-shot(经探针证 9/9)。
runtime-api-2d.md:补 clickable 组件文档。
**关键测量定论(终结本机迭代)**:便宜模型 tap-targets 单管线生成**高方差**(同 brief 一轮 8/9 一轮 5/9)——
N=1~2 小样 lili-mac 早读**信噪比不足**,无法可靠优化「模型产出过门率」(我前几轮在追噪声)。
**管线/运行时/约定已证正确(手写金样两品类均 9/9)+ 引擎补了 clickable 基元**;**模型产出 cutover 率是统计量**,
需大样+全 repair 预算 → **authoritative=mini-desktop SaaFullGraphE2eTest -Dsaa.e2e=1**(多 brief+5+3 repair+stage2)。
快走查工具:加 stage2 救场升档(QC_STAGE*)+ gamedef assert 字段 coerce 到 score(治 design 造 moves/totalRound 等取证读不到字段)。
设计决策待 6c6g:design↔gamedef 的 assertAfterPlay 字段词汇契约(design 应约束到 gamedef 暴露集 score/remaining,
或运行时暴露更多)——触共享 DESIGN_SYSTEM。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.4 KiB
运行时访问约定(2D 适配器 · behavior 侧 API)—— runtime-api-2d v0
plan 2026-06-18-001 U1 交付物。本文件是「写 gameDefinition 的 behavior 逻辑时能用的全部
rt面」的人/模型可读契约, U3 的GAMEDEF_SYSTEMprompt 直接嵌入本文(+ 末尾 few-shot)。实现见gd-runtime.js,类型雏形随后正式化为runtime-api-2d.d.ts。状态 v0(plan「别冻早」):已过 4 类原型表达力证(见
gd-archetypes.test.mjs),接口稳定但保留随真实模型输出补 idiom 的余量。 落点说明:与game-host.d.ts(第 9 类契约)同理,本约定是 game-runtime 内部「宿主↔游戏 behavior」接线面,故落game-runtime/src/host/; 是否升格为contracts/跨仓契约由 6c6g(大脑线/contracts owner)定。
1. 游戏 = 声明式 gameDefinition(改源不改打包产物)
一个游戏 = 一份 gameDefinition JSON(结构见 contracts/agent-loop/source-project.schema.json):
| 字段 | 含义 |
|---|---|
entities[] |
实体:{id, transform:{position:{x,y}}, components:[组件id...]}(可带 vx/vy/tags 供运行时用) |
components[] |
组件定义:{id, kind, ...},kind ∈ render/collision/physics/custom |
behaviors[] |
行为模块:{id, trigger, code}——code 是一段 JS 逻辑字符串,运行期编译执行 |
scenes[] |
场景:{id, entityRefs:[实体id...]}(v0 取 scenes[0] 决定初始实例化哪些实体;缺省全量) |
rules[] |
胜负规则:{id, condition, outcome}——condition 是 JS 布尔表达式串,outcome ∈ win/lose/score/advance |
核心范式:实体/组件/场景/规则是声明式数据;唯一的「逻辑代码」是 behaviors[].code 和 rules[].condition,
它们经受控的 rt 面操作世界。behaviors 只写逻辑、不写画(出图由声明式渲染器据 render 组件自动完成)。
2. behavior 代码契约
每个 behavior 的 code 被编译为 new Function('rt', 'self', 'dt', code),每次调用注入三参:
rt— 运行时访问面(见 §4),操作世界的唯一入口。self— 本 behavior 的持久局部态(普通对象{},跨帧保留)。存计时器/累加器:self.t = (self.t||0) + dt;dt— 本帧时间步(秒)。
trigger 语义:
init:世界建好后跑一次(布初始实体/初值)。update/input/collision/timer:每帧跑一次(输入经rt.input轮询;碰撞/计时在 behavior 内自查)。
硬约束(违反 = 生成缺陷):
- 确定性:随机一律
rt.random/rt.randRange/rt.randInt,时间一律rt.time.now()/dt——禁Math.random/Date.now/performance.now。 - 只用
rt面:禁自造约定外 helper(如rt._spawnFood)、禁document/window/裸引擎/requestAnimationFrame/addEventListener。要生成实体用rt.spawn。 - 终态置 latch:胜负用
rt.win()/rt.lose()(置不可逆终态),不要用计分或自定义 flag 表达「游戏结束」。 - 特效经
rt.fx:碰撞/得分等关键事件调rt.fx.burst(...)/rt.fx.beep(...)(真接引擎粒子/音频)。
边界硬拦截(非靠自觉):以上禁则在校验边界静态扫描——
behavior.code/rule.condition命中Math.random/Date/process/require/eval/.constructor/while(true)/document/window等,或 condition 含;/赋值/世界变更调用,会被拒绝并回灌 repair(不进编译)。模型代码只在浏览器执行 (esbuild 仅静态打包),故确定性/安全靠此扫描兜底——别用约定外手段,会被打回。
3. 规则(rules)
condition 是 JS 布尔表达式串,作用域内有 rt(与 self)。每帧求值;为真时按 outcome:
win/lose→ 置 latch 终态(一次性,不可逆)。score→ 上升沿 +1 分(防每帧重复加)。advance→ 场景推进(v0 占位)。
例:{ "id":"win", "condition":"rt.score >= 10", "outcome":"win" }
4. rt 面参考(全部可用 API)
实体
rt.getEntity(id)→ 实体或 null(仅活实体)rt.entities()→ 全部活实体数组rt.query(name)→ 按 tag 或组件 id/kind 过滤的活实体数组rt.spawn({x, y, vx?, vy?, tags?, components?})→ 新实体(components可内联组件对象);返回实体引用rt.destroy(entity)→ 标记死亡(帧末回收)- 实体字段:
.x .y .vx .vy .alive .tags(Set) .components(数组);方法.get(k)/.set(k,v)/.destroy();可直接读写任意属性(e.hp = 3)
输入(轮询)
rt.input.isDown(key)→ 是否按住(如'ArrowLeft'/'Space')rt.input.justPressed(key)→ 本帧是否刚按下rt.input.justTapped()→ 本帧是否发生指针按下rt.input.pointer→{x, y, down}
时间 / 随机(确定性)
rt.time.now()→ 相对游戏时间(秒,从 0 累加;用这个,非绝对钟)rt.dt→ 本帧步长(秒,= 注入的dt)rt.random()→[0,1);rt.randRange(a,b)→[a,b);rt.randInt(a,b)→[a,b]整数
分数 / 胜负
rt.score(读)/rt.addScore(n=1)/rt.setScore(n)rt.win()/rt.lose()→ 置 latch 终态
工具 / 特效
rt.clamp(v,lo,hi)/rt.dist(ax,ay,bx,by)/rt.overlap(a,b)(AABB,a/b={x,y,w?,h?},缺省半尺寸 16)rt.fx.burst(x,y,color)→ 引擎粒子(color 接'#rrggbb'或{r,g,b,a})rt.fx.beep(kind)→ 引擎音效(kind ∈score/hit/lose/win/default)rt.view→{w:390, h:844}(视口;边界判断用)
5. 组件(components)
- render:
{id, kind:'render', shape, color, ...}shape:'rect'+w,h(以实体 transform 为中心)shape:'circle'+rshape:'fill'+color(铺满视口,作背景)
- physics:
{id, kind:'physics', gravity?}——挂此组件的实体每帧自动x+=vx*dt; y+=vy*dt(有gravity则先vy+=gravity*dt)。不挂则位置全由 behavior 控制。 - clickable(离散点击声明式基元):
{id, kind:'clickable', score?:1, mark?:{shape,color,r/w/h}, hitW?, hitH?}——运行时内置:本帧点中带此组件且occupied!==true的实体(命中区取 hitW/hitH,缺省取 render 尺寸/80)→ 自动翻occupied=true+ spawnmark标记实体(可见变化) ++score+ fx 粒子/音效。离散点击类(井字棋/打地鼠/翻牌)用它即可,无需写 tap-handler(免模型把命中误门控在分离 state/计时器上)。配tags:["target"]让九门 driver 能逐个点。 - collision / custom:声明式标记,由 behavior 自行
rt.overlap判定 / 读取。
6. 原型范式(few-shot · 见 gd-archetypes.test.mjs 完整可跑版)
- paddle-intercept(打砖块/pong):板 entity + 球 entity(
vx/vy);update behavior 里键/指针控板、球积分+墙反弹、rt.overlap板拦截rt.addScore+rt.fx.burst、落底rt.lose()。 - event-clicker(点击器/放置):按钮 entity;input behavior 里
if(rt.input.justTapped()) rt.addScore(1);rulert.score>=N → win。 - dodge-spawn(躲避):玩家 entity;update behavior 里键控玩家、定时
rt.spawn随机位敌人(rt.randRange)、遍历rt.query('enemy')下落+出界rt.destroy+rt.overlap撞玩家rt.lose()。 - runner(跑酷):玩家挂
physics{gravity};update behavior 里rt.input.justTapped()起跳(置vy)、落地复位、定时rt.spawn障碍(vx)、rt.overlap撞障碍rt.lose()/ 出界rt.addScore。
6.1 取证/driver 命名约定(让九门能驱动你的游戏 · 关键)
九门 harness 据 gatespec 的 driver 用命名路径读取证状态来真玩你的游戏。运行时自动把世界投影成取证形状,但你必须命名/打标对齐,否则 driver 读不到 → 机制门必挂:
- 实体按 id 投影为顶层取证位:实体 id=
ball→ 取证可读ball.x/ball.y/ball.vx/ball.vy(含w/h/r/angle);id=paddle→paddle.x。 故 paddle-intercept 类:把球实体命名ball、挡板命名paddle(与设计 agent 的 gatespecdriver.ballPath="ball.x"/controlCheck.paddlePath="paddle.x"对齐)。 - 离散点击类(tap-targets):给每个可点目标实体打
tags:["target"],并(可选)设idx(序号)/occupied(是否已占)/safe(是否非致负); 运行时自动汇成targets:[{x,y,idx,occupied,safe}]供 driver 逐个点。不打 target 标 → driver 点不到 → H_progress 必挂。 - 保留取证键(
phase/score/result/remaining/progress/errors/entities/targets)不会被实体投影覆盖(同名实体 id 不投影)。
7. 常见坑(治真实模型漂移)
- ❌ 自造
rt._spawnFood等约定外 helper → ✅ 用rt.spawn(...)。 - ❌
Math.random()/Date.now()→ ✅rt.random()/rt.time.now()(确定性,否则取证不可复现 + 静态门拒)。 - ❌ 用分数/flag 表达「结束」 → ✅
rt.win()/rt.lose()置 latch。 - ❌ 在 behavior 里画图(
ctx.fillRect)→ ✅ 给实体加 render 组件,渲染器自动出图。 - ❌ 关键事件无特效 → ✅ 碰撞/得分调
rt.fx.burst/rt.fx.beep(满「真接线」门)。