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:
parent
fb01654311
commit
619351052d
@ -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 只见扁平件,结构性根除。〕
|
||||
|
||||
@ -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」逐条列 ❌→✅。
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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」逐条列 ❌→✅。
|
||||
|
||||
@ -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 抽其声明的方法名集(宽松:取所有「名(」形态,含泛型方法 名<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]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user