From 619351052df2e271acc621152d2cd233051627c0 Mon Sep 17 00:00:00 2001 From: lili Date: Mon, 29 Jun 2026 10:30:50 -0700 Subject: [PATCH] =?UTF-8?q?feat(cheap-gen):=20=E9=98=B6=E6=AE=B5=E4=B8=80B?= =?UTF-8?q?=20#1=20ctx.random/ctx.time=20=E6=94=B9=E5=8F=AF=E8=B0=83?= =?UTF-8?q?=E7=94=A8(=E5=8F=8C=20API=C2=B7=E5=90=91=E6=AF=8D=E8=AF=AD?= =?UTF-8?q?=E7=9B=B4=E8=A7=89,=E6=9C=80=E9=AB=98=E9=A2=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 附A #1(最高频·每游戏用):ctx.random/ctx.time 原是对象、须 .next()/.nowMs(),AI 直觉 ctx.random()/ ctx.time() 当函数调会抛错——prompt/形状门都教"是对象不是函数"当幻觉拦。改成双 API:既可直接调用 (ctx.time()=毫秒、ctx.random()=[0,1))又保留全部方法(.nowMs()/.elapsedMs()、.next()/.range()/.reseed()), 向后兼容(框架/插件内部仍用方法形态)。 单点实现:boot-game-host 复用 createHostDevContext(单一 context 源),故 buildContext 一处包装即覆盖 host-dev + 真游戏。加 _callableTime/_callableRandom 双 API 包装器,ctx 装配处 time/random 包装。 全链路同改:plugin.js(包装器+装配)+ tools.mjs(CTX_METHODS 加 time/random、API 门错误文案翻转) + prompt.mjs/cheap_roles.py 双源(调用形态翻转:两形态都对、但 ctx.nowMs() 漏 .time 仍错)+ skill:144。 验证:core plugin 31/31(核心不破)+ 确定性(ctx.random()/time() 可调用 + .next()/.range()/.reseed()/ .nowMs()/.elapsedMs() 双 API)+ 形状门 11/11 + [check] PASS(双形态同段)+ cheap-worker 16/16 + 双源一致。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .agents/skills/littlejs-game-dev.md | 2 +- cheap-worker/cheap_roles.py | 2 +- game-runtime/src/core/plugin.js | 33 ++++++++++++++++++++++-- game-runtime/tools/amodel-gen/prompt.mjs | 2 +- game-runtime/tools/amodel-gen/tools.mjs | 5 ++-- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.agents/skills/littlejs-game-dev.md b/.agents/skills/littlejs-game-dev.md index b0bdd290..2b4131ee 100644 --- a/.agents/skills/littlejs-game-dev.md +++ b/.agents/skills/littlejs-game-dev.md @@ -141,7 +141,7 @@ export function createGame({ plugins, bundle, viewport }) { - ❌ `update(dt)` 把 dt 当毫秒直接累加 `elapsedMs += dt` → ✅ **dt 单位是「秒」(≈1/60)**;游戏内毫秒计时(倒计时/spawn 间隔/耐心,常量如 60000/1300/4800)累加要 **`elapsedMs += dt * 1000`**。漏 ×1000 → 计时慢 1000 倍 → 永不到 spawn 阈值=零顾客、一局永不结束(实测 wanglanmei-v2 翻车点;`_template` 范例里 `remainMs - dt*1000` 就是这个换算)。 - ❌ `_forensicsView()` 在**外层**先算好 phase/targets 再闭包返回(`const phase=...; return { state(){ return { phase } } }`)→ ✅ **phase/targets/score 必须在 `state()` 函数体内部实时计算**(host 只在 boot 调一次 `_forensicsView()`,外层算的值定格成 boot 快照=永远 menu、targets 永远空 → 自动验收/驱动看不到目标、永判死菜单。实测 wanglanmei-v2:游戏真在 play 但门读到的 phase 恒 menu)。照 `_template/src/game-logic.js` 的 `state: () => ({ phase: sceneFsm.current(), ... })` 写。 - ❌ 在 game-logic.js 取受控面用 `opts.ctx` / `opts.boot` → ✅ 受控面在**实例 `init(boot)` 的 `boot.ctx`**(`boot.ctx.time` / `boot.ctx.random` / `boot.ctx.getEngine()` / `boot.ctx.log()`);**输入不经 `boot.ctx.getInput`——写实例方法 `handleTap(x,y)`/`handleKey(key)`,L1 派发**;`createGame` 入参是扁平 `{plugins,bundle,viewport}`,**没有 ctx**。 -- ❌ 修 bug 时图省事写 `Math.random()` / `Date.now()` / `setTimeout()` → ✅ `boot.ctx.random.next()` / `boot.ctx.random.range(a,b)`、`boot.ctx.time.nowMs()`、`timer-scheduler.after(ms,cb)`(红线 check 必拦;**越改越要守,thrash 时尤其别引入**)。 +- ❌ 修 bug 时图省事写 `Math.random()` / `Date.now()` / `setTimeout()` → ✅ `boot.ctx.random()` 或 `boot.ctx.random.next()` / `boot.ctx.random.range(a,b)`、`boot.ctx.time()` 或 `boot.ctx.time.nowMs()`、`timer-scheduler.after(ms,cb)`(#1:time/random 既可直接调用又保留方法,两形态都对;红线 check 必拦裸 Math.random/Date.now;**越改越要守,thrash 时尤其别引入**)。 - ✅ `sessionScore.add(n)` = `addScore(n)` 别名(#6 阶段一B:直觉名直接可用,两者等价);**其它方法名拿不准一律先 `read_file` 对应 api.d.ts**。 - ⚠️ **`host-config.js` 和 `game.js` 都是 L1 固定 plumbing,你不写**(amodel harness `write_file` 会拒)——**全 11 插件**(sceneFsm/sessionScore/hudUi/timerScheduler/save/gamefeel/juice/palettePost/audioMusic/collision/physics)+ viewport(390×844)+ 工厂 wiring 已替你配好。**你只写 `game-logic.js`(必)+ core/render/balance/assets**。〔历史教训 HJ-AGI-003:曾让 agent 写 host-config + game.js 工厂,M3 反复破坏返回契约(漏 viewport 崩 `reading 'w'`)/ registerOrder / 取插件漏 .plugins 崩 `reading 'define'`,故把 wiring 全收进 L1 固定、L3 只写游戏逻辑收扁平 {plugins,bundle,viewport},结构性根除。〕 - ❌ 在 game-logic.js 里写 `opts.runtime.plugins` / 把入参当 `opts` 处理 —— **新架构入参是扁平的 `{ plugins, bundle, viewport }`**(L1 的 game.js wrapper 已替你摊平 opts.runtime),写 `opts.runtime.*` 取到 undefined → boot 崩。✅ **直接 `export function createGame({ plugins, bundle, viewport }) { const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins; … }`**(plugins/bundle/viewport 都是入参、平级;无任何 opts/runtime 嵌套)。〔HJ-AGI-003:旧架构 M3 漏 `.plugins` 一层崩 `reading 'define'`,故把嵌套收进 L1 wrapper、L3 只见扁平件,结构性根除。〕 diff --git a/cheap-worker/cheap_roles.py b/cheap-worker/cheap_roles.py index c4acf76d..ea560fe1 100644 --- a/cheap-worker/cheap_roles.py +++ b/cheap-worker/cheap_roles.py @@ -43,7 +43,7 @@ A 读手册(1)+ 读你的起点 game-logic.js(4)→ B 按 brief 定玩 - game-logic.js 必须 **export function createGame({ plugins, bundle, viewport })**(命名导出、别改名、别默认导出);**零引擎 import**(引擎经 boot.ctx.getEngine())。 - 游戏源(game-logic / core / render)**零裸** Math.random / Date.now / setTimeout / requestAnimationFrame / new AudioContext / addEventListener —— 时间随机经 boot.ctx,定时经 timer-scheduler,音效经 plugins.audioMusic;**输入不调 ctx.getInput**(已收归 L1,见下「输入契约」)。 - update(dt) 内**必须调 bundle.tick(dt)**(bundle 是 createGame 入参;否则插件 onFrame 不推进、timer 永不到期、一局不结束)。**dt 单位是「秒」(≈1/60)**——游戏内若用毫秒计时(倒计时 / spawn 间隔 / 顾客耐心,常量都是 ms 如 60000/1300),累加时务必「elapsedMs += dt * 1000」,漏 ×1000 → 计时慢 1000 倍 → 永不到 spawn 阈值=零顾客、一局不结束。**timerScheduler.after(ms,cb)/every(ms,cb) 同样收毫秒(#7):3 秒写 after(3000)、别写 after(3);与 dt=秒相反,别混。** -- **入参是扁平的 { plugins, bundle, viewport }——没有 opts、没有 opts.runtime、没有 ctx**(L1 已替你摊平)。受控面 ctx 只在五法 **init(boot) 的 boot.ctx**;**调用形态钉死**:时间 **ctx.time.nowMs()**、随机 **ctx.random.next()/ctx.random.range(a,b)**、画布 ctx.getContext2d()、引擎 ctx.getEngine()、日志 ctx.log(tag,msg)。**time/random 是对象、不是函数——绝不写 ctx.nowMs()/ctx.random()**(实测翻车:当函数调 → 抛错 → 游戏空转不 spawn/不计分)。五法 = init(boot)/update(dt)/render(g)/destroy()(+必须 _forensicsView)+ **输入方法 handleTap(x,y)**(见下「输入契约」)。 +- **入参是扁平的 { plugins, bundle, viewport }——没有 opts、没有 opts.runtime、没有 ctx**(L1 已替你摊平)。受控面 ctx 只在五法 **init(boot) 的 boot.ctx**;**调用形态**:时间 **ctx.time()** 或 ctx.time.nowMs()、随机 **ctx.random()** 或 ctx.random.next()/ctx.random.range(a,b)、画布 ctx.getContext2d()、引擎 ctx.getEngine()、日志 ctx.log(tag,msg)。**#1:ctx.time/ctx.random 既可直接调用(ctx.time()=毫秒、ctx.random()=[0,1))、又保留方法(.nowMs()/.elapsedMs()、.next()/.range(a,b)/.reseed())——两形态都对**。**但 nowMs/next 是 time/random 上的方法、不是 ctx 上的——别写 ctx.nowMs()(应 ctx.time() 或 ctx.time.nowMs())。**五法 = init(boot)/update(dt)/render(g)/destroy()(+必须 _forensicsView)+ **输入方法 handleTap(x,y)**(见下「输入契约」)。 - **插件扁平注入:直接 const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins(用到才解构)**;**绝不写 opts.runtime.plugins**(那是 L1 内部的,你见不到 → 写了就 undefined → boot 崩 reading 'define')。游戏只用插件、不 new、不 register。 - **可用插件键(全 11 已注入,用到才取)**:sceneFsm / sessionScore / hudUi / timerScheduler / save / gamefeel / juice / palettePost / audioMusic / collision / physics。 - **插件方法名一律以 api.d.ts 为准、别臆测**(实测幻觉:scene-fsm 写 defineScenes〔应 define〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ 实测易犯的幻觉 API」逐条列 ❌→✅。 diff --git a/game-runtime/src/core/plugin.js b/game-runtime/src/core/plugin.js index f84c207b..a489311b 100644 --- a/game-runtime/src/core/plugin.js +++ b/game-runtime/src/core/plugin.js @@ -218,6 +218,33 @@ class HostDevTime { } } +/** + * #1 阶段一B:把受控 time 源包成「可调用 + 保留方法」的双 API(向母语直觉对齐:免记 .nowMs)。 + * ctx.time() === ctx.time.nowMs()(单调毫秒);.nowMs()/.elapsedMs() 保留(向后兼容,框架/插件内部仍用)。 + * @param {HostDevTime} t + * @returns {(() => number) & { nowMs: () => number, elapsedMs: () => number }} + */ +function _callableTime(t) { + const f = () => t.nowMs(); + f.nowMs = () => t.nowMs(); + f.elapsedMs = () => t.elapsedMs(); + return f; +} + +/** + * #1 阶段一B:把受控 random 源包成「可调用 + 保留方法」的双 API。 + * ctx.random() === ctx.random.next()([0,1) 确定性浮点);.next()/.range(a,b)/.reseed(seed) 保留。 + * @param {SeededRandom} r + * @returns {(() => number) & { next: () => number, range: (min: number, max: number) => number, reseed: (seed: number) => void }} + */ +function _callableRandom(r) { + const f = () => r.next(); + f.next = () => r.next(); + f.range = (min, max) => r.range(min, max); + f.reseed = (seed) => r.reseed(seed); + return f; +} + /** * host-dev 输入事件桥(Gate0.1)——InputSource 的本机/集成前实现。 * @@ -512,8 +539,10 @@ function createHostDevContext(opts) { if (typeof console !== 'undefined' && console.log) console.log('[' + entry.scope + ':' + entry.tag + ']', msg); } catch (_) { /* 日志失败不影响玩法 */ } }, - time, - random, + // #1 阶段一B:time/random 既可直接调用(ctx.time()=毫秒、ctx.random()=[0,1),向母语直觉) + // 又保留对象方法(ctx.time.nowMs()/elapsedMs()、ctx.random.next()/range(a,b)/reseed(),向后兼容、框架内部仍用)。 + time: _callableTime(time), + random: _callableRandom(random), }; return ctx; } diff --git a/game-runtime/tools/amodel-gen/prompt.mjs b/game-runtime/tools/amodel-gen/prompt.mjs index 72203048..6d7d0d49 100644 --- a/game-runtime/tools/amodel-gen/prompt.mjs +++ b/game-runtime/tools/amodel-gen/prompt.mjs @@ -86,7 +86,7 @@ A 读手册(1)+ 读你的起点 game-logic.js(4)→ B 按 brief 定玩法(核心 - game-logic.js 必须 **export function createGame({ plugins, bundle, viewport })**(命名导出、别改名、别默认导出);**零引擎 import**(引擎经 boot.ctx.getEngine())。 - 游戏源(game-logic / core / render)**零裸** Math.random / Date.now / setTimeout / requestAnimationFrame / new AudioContext / addEventListener —— 时间随机经 boot.ctx,定时经 timer-scheduler,音效经 plugins.audioMusic;**输入不调 ctx.getInput**(已收归 L1,见下「输入契约」)。 - update(dt) 内**必须调 bundle.tick(dt)**(bundle 是 createGame 入参;否则插件 onFrame 不推进、timer 永不到期、一局不结束)。**dt 单位是「秒」(≈1/60)**——游戏内若用毫秒计时(倒计时 / spawn 间隔 / 顾客耐心,常量都是 ms 如 60000/1300),累加时务必「elapsedMs += dt * 1000」,漏 ×1000 → 计时慢 1000 倍 → 永不到 spawn 阈值=零顾客、一局不结束(实测 wanglanmei 翻车点)。**timerScheduler.after(ms,cb)/every(ms,cb) 同样收毫秒(#7):3 秒写 after(3000)、别写 after(3);与 dt=秒相反,别混。** -- **入参是扁平的 { plugins, bundle, viewport }——没有 opts、没有 opts.runtime、没有 ctx**(L1 已替你摊平)。受控面 ctx 只在五法 **init(boot) 的 boot.ctx**;**调用形态钉死**:时间 **ctx.time.nowMs()**、随机 **ctx.random.next()/ctx.random.range(a,b)**、画布 ctx.getContext2d()、引擎 ctx.getEngine()、日志 ctx.log(tag,msg)。**time/random 是对象、不是函数——绝不写 ctx.nowMs()/ctx.random()**(实测 M3 翻车:当函数调 → 抛错 → 游戏空转不 spawn/不计分)。五法 = init(boot)/update(dt)/render(g)/destroy()(+必须 _forensicsView)+ **输入方法 handleTap(x,y)**(见下「输入契约」)。 +- **入参是扁平的 { plugins, bundle, viewport }——没有 opts、没有 opts.runtime、没有 ctx**(L1 已替你摊平)。受控面 ctx 只在五法 **init(boot) 的 boot.ctx**;**调用形态**:时间 **ctx.time()** 或 ctx.time.nowMs()、随机 **ctx.random()** 或 ctx.random.next()/ctx.random.range(a,b)、画布 ctx.getContext2d()、引擎 ctx.getEngine()、日志 ctx.log(tag,msg)。**#1:ctx.time/ctx.random 既可直接调用(ctx.time()=毫秒、ctx.random()=[0,1))、又保留方法(.nowMs()/.elapsedMs()、.next()/.range(a,b)/.reseed())——两形态都对**。**但 nowMs/next 是 time/random 上的方法、不是 ctx 上的——别写 ctx.nowMs()(应 ctx.time() 或 ctx.time.nowMs())。**五法 = init(boot)/update(dt)/render(g)/destroy()(+必须 _forensicsView)+ **输入方法 handleTap(x,y)**(见下「输入契约」)。 - **插件扁平注入:直接 const { sceneFsm, sessionScore, hudUi, timerScheduler } = plugins(用到才解构)**;**绝不写 opts.runtime.plugins**(那是 L1 内部的,你见不到 → 写了就 undefined → boot 崩 reading 'define')。游戏只用插件、不 new、不 register。 - **可用插件键(全 11 已注入,用到才取)**:sceneFsm / sessionScore / hudUi / timerScheduler / save / gamefeel / juice / palettePost / audioMusic / collision / physics。 - **插件方法名一律以 api.d.ts 为准、别臆测**(M3 实测幻觉:scene-fsm 写 defineScenes〔应 define〕)。用到某插件先 read_file 它的 api.d.ts;skill「⚠️ M3 实测易犯的幻觉 API」逐条列 ❌→✅。 diff --git a/game-runtime/tools/amodel-gen/tools.mjs b/game-runtime/tools/amodel-gen/tools.mjs index 0cb2a4be..d9ac655a 100644 --- a/game-runtime/tools/amodel-gen/tools.mjs +++ b/game-runtime/tools/amodel-gen/tools.mjs @@ -149,7 +149,8 @@ const PLUGIN_KEY_DIR = { audioMusic: 'audio-music', collision: 'collision', physics: 'physics-lite', }; /** PluginContext(boot.ctx)真方法集 + 运行时附加 log;time/random 是属性对象(ctx.time.nowMs()/ctx.random.next() 内层 time/random≠ctx,经"非键跳过"放行)。 */ -const CTX_METHODS = new Set(['getContext2d', 'onFrame', 'getInput', 'getAudioContext', 'getEngine', 'log']); +// #1 阶段一B:time/random 既是对象(.nowMs()/.elapsedMs()、.next()/.range()/.reseed())又可直接调用(ctx.time()/ctx.random()),故两者都计入合法 ctx 调用名。 +const CTX_METHODS = new Set(['getContext2d', 'onFrame', 'getInput', 'getAudioContext', 'getEngine', 'log', 'time', 'random']); const _apiCache = new Map(); /** 读某插件 api.d.ts 抽其声明的方法名集(宽松:取所有「名(」形态,含泛型方法 名(,over-permissive=少误报)。返回 Set|null。 */ function apiMethodSet(dir) { @@ -330,7 +331,7 @@ export function check(id) { const key = m[1], method = m[2]; if (key === 'ctx') { if (!CTX_METHODS.has(method)) { - errors.push(`API 静态门 ${f}: ctx.${method}() 不存在——boot.ctx 真方法 = ${[...CTX_METHODS].join('/')};时间用 ctx.time.nowMs()、随机用 ctx.random.next()(time/random 是对象不是函数)`); + errors.push(`API 静态门 ${f}: ctx.${method}() 不存在——boot.ctx 真方法 = ${[...CTX_METHODS].join('/')};时间 ctx.time()(或 .nowMs())、随机 ctx.random()(或 .next()/.range(a,b))——#1 后 time/random 既可直接调用又保留方法`); } } else if (PLUGIN_KEY_DIR[key]) { const ms = apiMethodSet(PLUGIN_KEY_DIR[key]);