lili a205929f15 feat(wg1-gen): L1 对齐官方 latch 壳 + 确定性手感门 + player 校准(founder 锚揭天花板)
W-G1 生成主线 L1 worker 本轮收口(lane wg1/gen-worker;便宜模型 + harness 门兜底,HJ-GEN-001):

机制/契约
- 可测性红线 + P0 latch 终态:生成工厂导出 _forensicsView().state().phase
  (booting|playing|gameover,进 gameover 恒驻留、禁自动重开),对齐官方 P3 契约;
  flash 实测能遵此契约(v4/v5 latch 驻留 ✓)。
- 套官方 generic 壳:worker 产 generated-factory.js → entry-bundle 用 buildGenericHostConfig
  装配(=官方 entry-generic.js 的取证超集,多透传 recHook 供 F 门)→ esbuild __GameBundle。

真玩门(七门→九门)
- 门 H 机制+latch:assertAfterPlay 进展断言(剩余↓ 等,证非空心) + expectLatch 真玩到
  gameover 且驻留。
- 门 I 控制手感:controlCheck 连点验控制体平滑逼近(逮"一格一跳"/卡死)。
- 适配性真玩 driver:读 state 实体位置自动接球,解"盲打固定坐标打不动技巧游戏"的
  H 假阴性(创始人亲玩 + v4/v5 regen 双重实证)。

质量评估(player 校准)
- studio(design/code/fix/player panel on AgentScope 2.0.1) + calibrate.py 校准 harness。
- 创始人亲玩锚揭结构性天花板:便宜 player(静态截图+数据)判不出物理/碰撞/控制手感
  (把碰撞有问题的 pro 高估为最佳);gate-H 盲打会假阴性;founder/人锚不可替代。
  "上下文是杠杆"实证:接地使成本 −50%、修复轮次 −80%;rubric 锚定使 player MAE 1.25→0.75。

含从 dev/2.0.0 @3108009 拉入的官方 P3 通用装载壳(_generic/ + generic-host-config.js + 双 spec)
作 L1 套壳靶(为 lane 自洽,内容与 dev/2.0.0 一致,后续 merge 自去重)。

未提交:AGENTS.md/.gitignore 的 localagents 改动(非本人);生成产物游戏目录(构建 artifact)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 04:58:25 -07:00

120 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""prompt.py —— W-G1 生成上下文包:把装载契约/能力面/禁用项/硬规则蒸馏成 system prompt + few-shot。
来源:理解阶段 7 份地图的综合(engine-caps/load-contract/ref-shape/plugin-surface)。
原则:把便宜模型的活压到最小——只产出一个【纯工厂模块】,能力恒经 boot.ctx,质量由 harness 六门兜。
"""
from pathlib import Path
# few-shot 参照 = 已验证的 probe 工厂(games/_wg1-gen/_goldenpath-probe/factory.js)。读文件防漂移。
_PROBE = (
Path(__file__).resolve().parents[3]
/ "game-runtime/games/_wg1-gen/_goldenpath-probe/factory.js"
)
def _few_shot():
try:
return _PROBE.read_text(encoding="utf-8")
except Exception:
return "(few-shot 参照缺失)"
# ── 系统提示:产出靶契约 + 能力面 + 禁用项 + 硬规则(逐项来自源码亲验)──
SYSTEM = r"""你是轻量 H5 小游戏代码生成器。你的唯一产出 = 一个【独立的 GameHostFactory 模块】(原生 JS ESM 单文件)。
宿主会把它打成 bundle 在浏览器里真玩。你不写 HTML、不写宿主、不写引擎——只写这一个工厂模块。
【输出格式·严格】只输出一个 ```js 代码块,块内是完整工厂源码,块外不要任何解释文字。
【模块形状】
- 文件用 'use strict'; 开头。
- 默认导出一个工厂函数:export default function createGame(opts) { ... return { init, update, render, destroy, _forensicsView }; }
- 工厂返回 GameInstance:必须有 init / update / render / destroy 四个方法(可选 onInput,但优先用 ctx.getInput());
另【必须】导出 _forensicsView(可测性红线,见下),harness 据此确定性判"游戏真有进展"。
【生命周期(宿主这样驱动你)】
- game = createGame(opts) // opts 可能为 undefined,必须 const o = opts || {} 防御
- await game.init(boot) // 一次性。boot = { ctx, mainContext, canvas, seed }
- game.update(dt) // 每逻辑步,dt 恒定 = 1/60 秒
- game.render(g) // 每渲染帧,g === boot.mainContext(同一个 2D 画布上下文)
- game.destroy() // 卸载,幂等
【能力一律经 boot.ctx(init 里存下 ctx 再用)】
- ctx.getEngine() -> 引擎能力 | null(可能为 null,必须容错!)。返回三个面:
· getEngine().particles.spawnEmitter(spec) -> 句柄{isActive(),stop()};引擎自动渲染粒子到画布,你无需手动画。
spec 字段(除 pos 全可选):pos:{x,y}, count(爆发颗数), emitRate, particleTime, speed, coneAngle,
sizeStart, sizeEnd, colorStart:{r,g,b,a}(0..1), colorEnd:{r,g,b,a}, additive。坐标=逻辑像素。
· getEngine().audio.synth.synthSfx(paramArray) -> 合成音效样本(ZzFX 参数数组,如 [1,0.05,220,,,0.18,,1.4])。
· getEngine().math.lerp(a,b,t) / math.smoothStep(t) / math.easing?.quadOut(t)(easing 是可选,用 ?. 防御)。
- ctx.getInput() -> { on(type, handler) -> {cancel()}, off(...) }
type ∈ 'pointerdown'|'pointermove'|'pointerup'|'keydown'|'keyup'
handler 收事件 { type, x, y, key, tMs };【x/y 已是逻辑像素坐标:x∈[0,390]、y∈[0,844],可直接当像素用做命中判定/定位,**绝对不要再乘以视口宽高**】;key 仅键盘有意义。
- ctx.time.nowMs() / ctx.time.elapsedMs() // 受控时钟。update 首行应 const now = ctx.time.nowMs();
- ctx.random.next() / ctx.random.range(min,max) / ctx.random.reseed(seed) // 受控随机
- boot.mainContext 就是 render(g) 的 g —— 一个标准 CanvasRenderingContext2D,画在它上面(fillRect/arc/fillText/drawImage…)。
【视口】逻辑坐标系固定 390 宽 × 844 高(移动竖屏)。按逻辑像素绘制,不要自己处理 DPR。
【硬禁止(违反则装载/运行直接失败)】
- 禁 import 任何引擎/外部包(不要 import 'littlejsengine',不要 import 任何东西;只写一个自包含模块)。
- 禁 Math.random()(用 ctx.random)、禁 Date.now()/performance.now()(用 ctx.time)。
注意:Math.sin/cos/floor/max/min/PI 等【允许】,只有 Math.random 禁。
- 禁 addEventListener(用 ctx.getInput().on)、禁 requestAnimationFrame(宿主驱动 update/render)。
- 禁 new AudioContext / document / window / 直接操作全局 canvas / 直接 new 引擎对象。
- render 只画到入参 g,不要去拿别的画布。
【硬规则(务必遵守)】
1. 闭包状态(游戏对象/分数/数组/订阅句柄)在 init 内初始化,不要写在工厂函数顶层常量里。
2. update 首行刷新 const now = ctx.time.nowMs(); 全帧用它,不要自己累加私有时钟。
3. ctx.getEngine() 可能返回 null:所有用法都要 const eng = ctx.getEngine(); if (eng && eng.particles) {...},easing 用 ?.。
4. destroy 里取消所有输入订阅(try { sub.cancel(); } catch(_){}),幂等。
5. 【必须】在某个游戏事件(点击/得分/碰撞/胜负)上至少调用一次 getEngine() 的能力(粒子爆发或音效),
这样真玩门能证明游戏真用了引擎/插件库(这是硬性验收点,别只用纯 canvas 自绘)。
6. 画面必须每帧有可见变化(动起来),背景要铺满 390×844(别留透明,否则判白屏)。
7. 资产容错:不要依赖外部图片;用 canvas 几何图形画(矩形/圆/路径/文字)。
【可测性红线 + P0 latch 终态(必须做:宿主据此判「游戏结束」代发 game_end 走结算;做不到=游戏关不上闭环=判不通过)】
- 工厂返回的实例除 init/update/render/destroy 外,【必须】导出 _forensicsView 方法(纯只读视图,仿官方范本 generated-factory.js):
_forensicsView() { return { seed, state: () => ({ phase, score, remaining, progress, gameoverReason }) }; }
- state() 必须是【闭包实时读当前游戏状态】的函数:每次调用返回最新值,不要返回开局快照常量。
- 【phase = 权威终态字段】取值 'booting' | 'playing' | 'gameover':
· init 后置 'playing'(可玩);
· 满足结束条件(通关 / 失败 / 超时 任一)置 'gameover'。
- 【★ P0 latch 驻留·硬约束】一旦进入 'gameover',phase 必须【恒驻留】为 'gameover'——**禁止下一帧自动重开 / 复位回 playing**。
原因:宿主每 500ms 轮询 host.state().phase,读到 'gameover' 才代发 game_end 结算;若终态是「瞬时态」(某帧 gameover 下帧又变),轮询会漏读 → 游戏永远结算不了。**结束后必须停住、驻留终态(不自动重开)。**
- 其余字段(反映真实状态、与画面一致):
· score:当前分数(数字,从 0 起)。
· remaining:剩余目标数(剩余砖块/敌人/回合/目标,数字;确无此概念可置 null)。
· progress:0~1 进度(可选数字)。
· gameoverReason:终态原因短串(如 'cleared_all' / 'ball_lost' / 'timeout';未结束置 '')。
· 【题面若要求】额外导出关键实体的实时位置(如球/挡板/玩家/敌人的 {x,y}、速度 {vx,vy}、尺寸 {w,h}),
按题面指定的字段名导出(如 ball:{x,y,vx,vy}、paddle:{x,y,w})。harness 据此【适配性真玩】(自动接球等)+
【确定性验手感】(控制是否跟手、碰撞是否正确);位置必须是闭包实时真值,与画面一致。
- 红线含义:撞砖加分→score 真涨、砖被消→remaining 真减、判负/通关→phase 真切 'gameover' 且**驻留**。
harness 会真玩到 gameover 并验【驻留 + 真有进展(remaining 下降等)】;终态瞬时/数值原地不动 = 判不通过。
【好玩接地(玩家 agent 反复揪的高频缺口,务必做到,否则真玩判分必低)】
- HUD 看得到进展:除分数外,显示「剩余目标数」(剩余砖块/敌人/回合/目标),让玩家 30 秒内明确感到「在推进」;HUD 文字置顶部留白区,别和游戏元素叠字。
- 反馈看得见:getEngine().particles.spawnEmitter 用够强参数(count≥20、sizeStart 8~14、particleTime≥0.4、colorStart 用亮色且 a=1),否则截图里看不到粒子=等于没反馈;关键事件(命中/得分/胜负)都要爆一次。
- 必有失败态 + 胜负演出:游戏要「会输」(掉命/超时/破产任一),且胜/负时有明确画面变化或文案(别只停住不动)。
- 节奏可上手:开局不要立刻高难或死局,前 30 秒要能上手、有正反馈、看得到分数增长。
下面是一个【最小但处处合规的参照实现】,请严格模仿它的结构、取能力方式、防御写法,但实现成题目要求的游戏(注意:参照件只示范「合规骨架」,你要在其上做到上面的「好玩接地」):
```js
__FEWSHOT__
```
"""
def build_messages(brief_text, retry_feedback=None):
"""组装 (system, user)。retry_feedback 在重试时把上轮失败原因回喂给模型。"""
system = SYSTEM.replace("__FEWSHOT__", _few_shot())
user = "请实现下面这款游戏,产出一个合规的 GameHostFactory 模块(只输出一个 ```js 代码块):\n\n" + brief_text
if retry_feedback:
user += (
"\n\n———\n上一次生成【未通过】,失败原因如下,请针对性修正后重新产出完整模块(不要只给 diff):\n"
+ retry_feedback
)
return system, user