lili 80aaa116ab feat(game-runtime): clickable 声明式点击基元(harness 探针证 9/9)+ tap-targets prompt 迭代——治便宜模型 tap-handler 顽疾
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>
2026-06-18 06:13:18 -07:00

9.4 KiB
Raw Blame History

运行时访问约定(2D 适配器 · behavior 侧 API)—— runtime-api-2d v0

plan 2026-06-18-001 U1 交付物。本文件是「写 gameDefinition 的 behavior 逻辑时能用的全部 rt 面」的人/模型可读契约, U3 的 GAMEDEF_SYSTEM prompt 直接嵌入本文(+ 末尾 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 内自查)。

硬约束(违反 = 生成缺陷):

  1. 确定性:随机一律 rt.random/rt.randRange/rt.randInt,时间一律 rt.time.now()/dt——禁 Math.random / Date.now / performance.now。
  2. 只用 rt 面:禁自造约定外 helper(如 rt._spawnFood)、禁 document/window/裸引擎/requestAnimationFrame/addEventListener。要生成实体用 rt.spawn。
  3. 终态置 latch:胜负用 rt.win()/rt.lose()(置不可逆终态),不要用计分或自定义 flag 表达「游戏结束」。
  4. 特效经 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' + r
    • shape:'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 + spawn mark 标记实体(可见变化) + +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);rule rt.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 的 gatespec driver.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(满「真接线」门)。