feat(cheap-gen): 阶段一B #1 ctx.random/ctx.time 改可调用(双 API·向母语直觉,最高频)

附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) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-29 10:30:50 -07:00
parent fb01654311
commit 619351052d
5 changed files with 37 additions and 7 deletions

View File

@ -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 只见扁平件,结构性根除。〕

View File

@ -43,7 +43,7 @@ A 读手册1+ 读你的起点 game-logic.js4→ 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) 同样收毫秒#73 秒写 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)**#1ctx.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.tsskill 实测易犯的幻觉 API逐条列

View File

@ -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.1InputSource 的本机/集成前实现
*
@ -512,8 +539,10 @@ function createHostDevContext(opts) {
if (typeof console !== 'undefined' && console.log) console.log('[' + entry.scope + ':' + entry.tag + ']', msg);
} catch (_) { /* 日志失败不影响玩法 */ }
},
time,
random,
// #1 阶段一Btime/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;
}

View File

@ -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逐条列

View File

@ -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 阶段一Btime/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 抽其声明的方法名集(宽松:取所有「名(」形态,含泛型方法 名<T>(,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]);