zizi 3108009500 feat(runner-v2): P3 L0 通用装载壳——L1 生成 worker 的产出靶载体(node 5 门验过)
P3 第一块基石:把 wanglanmei-ref/entry-bundle.js 的「studio-consumable 引擎包入口」泛化为「任意生成游戏」通用壳——L1 worker 只写 generated-factory.js(一个 GameHostFactory),套本壳 esbuild 打 __GameBundle bundle 即得 studio 可消费引擎包,避免 20 个 prompt 各造一遍 7 插件 wiring。

新增(零改既有契约):① src/host/generic-host-config.js=buildGenericHostConfig(标准 7 能力插件集+六锚偏序+390×844 视口+wiring,零wanglanmei专属);② games/_generic/entry-generic.js=通用入口壳(动态 import 引擎 Option C node-safe 与 entry-bundle 同款;调用形 {canvas,seed} 逐字对齐 inject.ts:248;暴露 window.__GameBundle.{GameHostFactory,bootGameHost,default});③ games/_generic/generated-factory.js=占位 stub(L1 出码替换靶)+ P0 latch 终态范本(到 TARGET 置 gameover 驻留不可逆,_forensicsView().state().phase 暴露给 watchGameEnd);④ test/generic-shell.test.mjs=node 侧守门零依赖。

5 门验过(6c6g node,主笔亲跑复核):①esbuild iife __GameBundle.bootGameHost=function ②import-safe(node-load window 零触)③回归 src 161/161+games 57/57 ④B1 raw 203.8KB/gz 69KB ✔ ⑤占位工厂经受控面 init→tap→latch gameover 驻留。dist/ 已 gitignore 不入库。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:40:35 +00:00

179 lines
9.2 KiB
JavaScript
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.

/**
* generated-factory.js — 【占位 stub】生成工厂占位(L1 出码替换本文件)
* owner:P3/W-G1 生成主线 L0 | 消费方:entry-generic.js(import 本默认导出作 GameHostFactory)+ L1 生成 worker(出码替换靶)
*
* ════════════════════════════════════════════════════════════════════════════
* 【占位说明 · 务必先读】
* 本文件是 L0 通用装载壳的「占位工厂」——用途仅为让 entry-generic.js 立即可 esbuild + 可启动验证。
* **L1 生成 worker 出码时整文件替换本文件**,写入真实生成游戏的 GameHostFactory(默认导出)。
* 本占位同时是 L1 的「最小范本」:演示三条硬约束——
* ① 纯工厂、零引擎 import(引擎能力一律经 boot.ctx.getEngine(),本占位极简未用引擎能力,但保持零 import);
* ② 实现并默认导出一个 GameHostFactory((opts?) => GameInstance,对齐 game-host.d.ts:59);
* ③ **P0 latch 终态**:到 N 分进入 'gameover' 并**驻留**(不自动重开),经 _forensicsView().state().phase 暴露。
*
* 【为何必须演示 latch 终态(P3 硬约束)】
* studio host 经 watchGameEnd 轮询 host.state().phase(inject.ts:217),到 'gameover' 代发 game_end{completed:true}
* (门② 闭环锚)。轮询是「间隔采样」——若终态是「瞬时 phase」(某帧置 gameover 下帧又变),轮询会漏报致闭环静默断裂
* (见 memory runner-v2-p1-loading-contract P3 硬约束)。故终态必须**驻留**:一旦 gameover,state().phase 恒为 'gameover'。
*
* 【极简玩法】点击画布计分;到 TARGET_SCORE 分进入 gameover 终态驻留。无引擎能力依赖,可在 stub/real/node 任一通道跑。
* ════════════════════════════════════════════════════════════════════════════
*/
'use strict';
/** 目标分(点击累计到此分进入终态)。占位用极小值,便于启动验证快速到达终态。 */
const TARGET_SCORE = 3;
/**
* 【装载契约 GameHostFactory】建占位游戏实例(纯工厂,零引擎 import)。
* @param {Object} [opts] 工厂选项(host wiring + additive 扩展位;本占位不用 runtime,演示纯净工厂)。
* @returns {import('../../src/core/game-host.d.ts').GameInstance}
*/
export default function createGenericPlaceholderGame(opts) {
// 本占位不复用任何能力插件(演示「纯净工厂仅吃 boot 脊柱」路径);opts.runtime 留给真实游戏用。
void opts;
/* ── 闭包态(init 内由 boot 填充;render 读绘制面)── */
/** @type {import('../../src/core/game-host.d.ts').GameHostBootContext|null} */
let boot = null;
/** @type {CanvasRenderingContext2D|null} 绘制面(=boot.mainContext,render 的 g 同此面)。 */
let mainCtx = null;
let seed = 0;
// 受控输入订阅句柄(destroy 时取消,幂等)。
let subTap = null;
// 游戏态:分数 + 阶段('playing' → 'gameover' latch 驻留)。
let score = 0;
/** @type {'booting'|'playing'|'gameover'} */
let phase = 'booting';
let elapsedMs = 0; // 逻辑时长(update 累计;仅作旁证,不驱动终态)。
/**
* 计一次点击分:仅 playing 阶段加分;到 TARGET_SCORE 进入 gameover 并驻留(latch,不可逆)。
* 这是 P0 latch 终态的核心——一旦置 gameover,本函数后续点击不再改 phase(终态驻留)。
*/
function scoreOne() {
if (phase !== 'playing') return; // 终态/未就绪不计分(gameover 后点击无效=驻留)
score += 1;
if (score >= TARGET_SCORE) {
phase = 'gameover'; // latch:置入终态后恒为 gameover(watchGameEnd 轮询任一时刻读到的都是 gameover)
}
}
/* ════════════════════════════════════════════════════════════════════
* 装载契约 GameInstance 五件(init/update/render/onInput?/destroy)
* ════════════════════════════════════════════════════════════════════ */
/**
* init(boot):建游戏对象、订阅受控输入、保存绘制面。boot=装载契约脊柱 {ctx, mainContext, canvas, seed}。
* @param {import('../../src/core/game-host.d.ts').GameHostBootContext} b
*/
function init(b) {
boot = b;
mainCtx = b.mainContext; // render 的绘制面(real=引擎 mainContext / stub=可见画布 ctx)
seed = b.seed >>> 0;
score = 0;
elapsedMs = 0;
phase = 'playing'; // init 后即可玩
// 订阅受控输入:每个 pointerdown 计一次分(经 boot.ctx.getInput(),不触裸 DOM)。
// ctx.getInput().on 返回订阅句柄(含 cancel),destroy 时取消(与 ref 同口径)。
try {
const input = boot.ctx.getInput();
if (input && typeof input.on === 'function') {
subTap = input.on('pointerdown', () => scoreOne());
}
} catch (_) {
// 输入面缺失(极简通道)不致命:onInput 备选入口仍可计分(见下)。
subTap = null;
}
}
/**
* update(dt):逻辑步(dt 秒,引擎恒定步长)。占位仅累计逻辑时长作旁证;终态由点击驱动,update 不改 phase。
* @param {number} dt
*/
function update(dt) {
if (phase === 'booting') return;
elapsedMs += (typeof dt === 'number' ? dt : 1 / 60) * 1000;
}
/**
* render(g):绘制段(g=boot 注入的绘制面)。占位画极简文本(分数/阶段),证明绘制面可用。
* 注:占位用 fillText 简画;真实生成游戏在此画美术(引擎粒子已自渲落同一 g)。
* @param {CanvasRenderingContext2D} g
*/
function render(g) {
const ctx2d = g || mainCtx;
if (!ctx2d) return;
try {
// 极简铺底(占位:纯色背景,证明绘制面可写)。坐标按逻辑像素(host 已在 real 外层补 DPR 变换)。
ctx2d.fillStyle = phase === 'gameover' ? '#2b1a1a' : '#10243a';
ctx2d.fillRect(0, 0, 390, 844);
ctx2d.fillStyle = '#e8e8e8';
ctx2d.font = '24px sans-serif';
ctx2d.fillText(phase === 'gameover' ? 'GAME OVER' : ('SCORE ' + score), 24, 64);
ctx2d.font = '14px sans-serif';
ctx2d.fillText('tap to score · target ' + TARGET_SCORE, 24, 96);
} catch (_) {
// 绘制面只读/异常不致命(占位绘制非验收项;终态判据走 state().phase)。
}
}
/**
* onInput(ev):可选输入入口(不便用 ctx.getInput 的极简通道备选)。pointerdown 计分。
* 与 init 内 ctx.getInput().on 订阅二者择一生效;host 通常走 ctx.getInput(),此入口兜底极简通道。
* @param {{type:string, x?:number, y?:number, key?:string}} ev
*/
function onInput(ev) {
if (ev && ev.type === 'pointerdown') scoreOne();
}
/**
* destroy():卸载卫生(取消输入订阅,幂等)。P3 装载多游戏复位用。
*/
function destroy() {
try { if (subTap && subTap.cancel) subTap.cancel(); } catch (_) { /* 幂等 */ }
subTap = null;
boot = null;
mainCtx = null;
phase = 'booting';
}
/* ════════════════════════════════════════════════════════════════════
* GameInstance + host wiring 扩展面
* _forensicsView / _handleAction 给 host 取证与 host.do/state() 用(非契约成员,host wiring)。
* host.state() 读 _forensicsView().state().phase → watchGameEnd 据此判终态(必须暴露 phase)。
* ════════════════════════════════════════════════════════════════════ */
return {
// ── 装载契约 GameInstance 五件 ──
init,
update,
render,
onInput,
destroy,
// ── host wiring 扩展(非契约成员)──
/**
* @internal host 取证:返回 forensics 视图。host.state() 透传 state()(含 phase,watchGameEnd 判终态依赖)。
* state() 返回 JSON 安全快照;phase 为 latch 终态(gameover 驻留)。
*/
_forensicsView: () => ({
seed,
state: () => ({
phase, // 'booting' | 'playing' | 'gameover'(gameover 后驻留,watchGameEnd 轮询读到恒为 gameover)
score,
target: TARGET_SCORE,
elapsedMs,
gameoverReason: phase === 'gameover' ? 'reached_target_score' : '',
}),
}),
/**
* @internal host wiring:host.do(action) 透传到游戏逻辑公开 API。
* 占位支持 {type:'tap'} 计分(e2e 辅助;与 UI 点击同走 scoreOne 逻辑公开 API)。
*/
_handleAction: (action) => {
if (action && action.type === 'tap') scoreOne();
},
};
}