- .agents/skills 25 件全量 frontmatter 规范化与评审修入(含 prompt-governance 大修);.claude/skills 7 件薄壳按双层方案①落位 - 新增 skill:agentic-seat-context-design(agentic 席位与 context 工程设计基线,2026-07-05 探索蒸馏) - 设计波三件落档:复杂游戏北极星件(W-NSTAR 终审稿待拍)/黄金模板规格件(W-TPL 定稿待批)/生成侧过程蒸馏回路(W-GENLOG 骨架) - protocol/在飞板/作战清单/数据飞轮 SoT/契约 prompts 索引同步;breakout 九门证据刷新 - .gitignore 补 /localagents.md 真实忽略行(该文件自声明绝不提交,此前声明未被机器执行) - 刻意不入库:nacos-data/ 与 _tier2-gen、c2v-*、amgen-* 生成产物(可重生成,忽略行格式待拍) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
24 KiB
name, description
| name | description |
|---|---|
| littlejs-game-dev | 当生成 agent 要写一款可上线 LittleJS 小游戏的 game-logic.js(接五法、调 11 注入插件、守受控面红线、mmx 产素材)时使用:代码分层结构、写什么 vs 调什么、插件 API 速查与 M3 易犯幻觉、资产放置。轻中档 code 层唯一作业手册。 |
skill: littlejs-game-dev —— AI 直接写 LittleJS 游戏代码(调插件 API,不重写实现)
定位:这是 生成 agent 造一款可上线的高质小游戏的唯一作业手册。绘境AI 的终态产物 =
src/多文件 LittleJS 工程 (创始人 2026-06-20 拍板;取代已废的「gameDefinition JSON 投影」线)。本手册回答四件事: ① 代码长什么结构 ② 你写什么 / 调什么 ③ 怎么调 11 注入插件 API(+ runtime-probe 仅取证不注入)④ 美术/音乐/音效放哪、怎么用 mmx 生成。这一档定位(钉死):轻量 ≠ 简单。AI 参与深度低,但产物是高质小游戏——复杂能力由工程脚手架 + L2 插件库承担,你少写、多调插件做出卖相与深度,不是产没人会玩的简单玩具。
配套:契约匹配范例 =
game-runtime/games/_template/(克隆起点,签名以现契约为准 —— 照它的结构改造,别抄旧 wanglanmei-ref 的 game-logic,那是旧 opts 签名漂移); 插件契约 SoT = 各game-runtime/src/plugins/<name>/api.d.ts(签名以它为准);装载契约 =game-runtime/src/core/game-host.d.ts。
0. 三句话心法
- 你只写「玩法身份(WHAT)」,通用机制(HOW)一律调插件——能调就别自己实现。 写碰撞数学/粒子/补间/计分/状态机 = 错;这些是插件。
- 你的唯一产出靶 =
game-logic.js的命名导出createGame({plugins,bundle,viewport}),返回 GameInstance:init/update/render/destroy+ 输入方法handleTap(x,y)[/handleKey(key)] +_forensicsView()。零引擎 import。 - 受控面铁律:游戏里 绝不出现
Date.now/performance.now/Math.random/setTimeout/requestAnimationFrame/new AudioContext/import 'littlejsengine'。时间/随机/音频/引擎能力一律经boot.ctx或注入的插件拿;输入不调ctx.getInput——写实例方法handleTap(x,y)/handleKey(key),L1 据此在 pointerdown/keydown 时派发(确定性、可取证、引擎可换)。
1. 代码结构(可导航多文件)
一款游戏 = 一个目录,固定分层。每个文件单一职责,改一处不牵动全身:
这条「改一处不牵动全身」的工程规范性正是 A11 对话式调整回路的可改性根基——资产统一
assets.js、数值集中core.js/balance.js,使一次自然语言调整只落到"那一处"(换资产/调数值 = 改一处、零 LLM;改玩法 = 有界单文件重写)。A11 回路机制(两段式判意图/执行 + 三断言)见../knowledge/tech-decisions.md§1.2。
games/<your-game>/
├── entry.js # 打包入口:动态 import 引擎 → 委托 main.js(引擎 import 只在这)
├── index.html # 加载 dist bundle,调 bootXxx 启动
├── src/
│ ├── main.js # host 引导薄包装:调通用 bootGameHost,装配取证全局
│ ├── host-config.js # 【L1 固定·不写】实例化全11插件 + 视口 + 工厂 wiring(harness 已配好、write 拒)
│ ├── game.js # 【L1 固定·不写】薄 wrapper:摊平 opts.runtime→交 game-logic(write 拒)
│ ├── game-logic.js # 【L3·你写·必写】export function createGame({plugins,bundle,viewport})→GameInstance 五法。零引擎 import
│ ├── core.js # 【L3·推荐】纯逻辑:状态机 + 命令 API(无 I/O/引擎,node 可单测;小游戏可内联进 game-logic)
│ ├── balance.js # 【内容】数值 / 关卡 / 价格 / 掉落表(纯数据 + 公式)
│ ├── render.js # 【绘制】把状态画到 g;导出视口 W/H 常量
│ ├── assets.js # 【资产声明】图键→文件名映射 + loadAssets(host 层,可用 Image)
│ └── audio-director.js # 【音频编排】BGM/音效调度(可选)
├── assets/
│ ├── manifest.json # 资产清单:file/role/bytes/sha256/source{tool,model,prompt}
│ ├── gen-ledger.md # 生成历史(prompt/字节/hash/替换记录)——改素材的索引
│ └── *.jpg *.mp3 # 美术 / 音乐 / 音效(mmx 产物)
└── test/
├── *.test.mjs # node 纯逻辑测试(core/balance)
└── *.cdp.cjs # e2e 真玩取证(九门)
分层依赖方向(只能往下依赖,不能反向):
entry-bundle → host-config → game(L1 wrapper)→ game-logic(你写)→ {core, render, balance, assets};core/balance 最纯(零依赖、可单测)。
别把 4000 行的 wanglanmei-ref 整个抄过来——它是「进阶worked 参照」。克隆
_template/、follow 这个结构,逻辑写进core.js。
2. 写什么 vs 调什么(这是本手册的核心)
| 层 | 谁负责 | 具体 |
|---|---|---|
| 玩法身份 WHAT | 你写 | game-logic.js(必)把玩法接到五法 + 调注入的插件 · core.js 状态机与规则 · balance.js 数值/关卡/内容 · render.js 画面编排与命中区布局 · 事件→表现映射(何时触发哪个 juice/音效) · assets.js 资产声明 |
| 通用机制 HOW | 调插件 | 碰撞 / 物理 / 手感 / 缓动 / 输入缓冲 / 粒子打击感 / 音频 / 存档 / 调色 / 计分胜负 / HUD-UI / 场景状态机 / 定时调度(共 11 注入件,见 §4;另 runtime-probe 仅取证不注入) |
| 装载 + 取证 | 平台供给 | bootGameHost 引擎五回调装载 · 插件注入 · boot.ctx.getEngine() · runtime-probe 六锚取证 |
判定口诀:"这段是『这款游戏特有的规则/内容/画面』吗?" 是 → 你写;"这是『换个游戏也一样』的通用能力吗?" 是 → 找插件调。找不到对应插件再自己写(并反馈:它可能该被沉淀成新插件)。
3. 装载契约(你的产出靶)
游戏工厂 = GameHostFactory(game-host.d.ts:66),host 调它建 GameInstance:
// src/game-logic.js —— 【你写这个】命名导出 createGame,零引擎 import
// 入参由 L1(game.js wrapper)替你摊平:扁平 { plugins, bundle, viewport },没有 opts.runtime 嵌套。
export function createGame({ plugins, bundle, viewport }) {
// 插件已扁平注入(L1 host-config 替你装好全 11 件),用到才解构:
const { sceneFsm, sessionScore, hudUi, timerScheduler /* …按需 gamefeel/juice/collision/physics/audioMusic */ } = plugins;
const core = createCore(); // 你的纯逻辑(可内联进本文件)
return {
async init(boot) {
this.ctx = boot.ctx; // 受控面:time/random/getEngine/log(只在 init(boot) 拿;输入不在此订阅)
this.assets = await loadAssets('./assets/'); // 容错:缺图降级不崩
},
update(dt) { bundle.tick(dt); core.update(dt); }, // ★ 必调 bundle.tick(驱动插件 onFrame)
render(g) { drawFrame(g, core.state(), this.assets, hudUi, viewport); }, // g=引擎 mainContext;读 viewport.w/h
handleTap(x, y) { core.tap(x, y); }, // ★输入入口:L1 在 pointerdown 时直达调用(别自己订阅输入)
destroy() {}, // 卸载(幂等)
};
}
要点:
- render(g) 的
g=boot.mainContext(引擎绘制面);绘制画在g上,不要用插件的getContext2d()(那是另一块 overlay)。 - 插件扁平注入:直接
const { sceneFsm, … } = plugins(plugins是 createGame 入参;不是opts.runtime.plugins—— 那是 L1 内部的、你见不到,写了就 undefined → boot 崩)。游戏只「用」,不new、不register。 - 时间/随机/引擎走
boot.ctx(init(boot)内拿;入参里没有 ctx)。 - 输入收归 L1(别自己订阅):不写
ctx.getInput()、不自建点击队列、不在update里挑时机消费点击——只写实例方法handleTap(x,y)(点击)/handleKey(key)(键盘),L1 的 game.js wrapper 在 pointerdown/keydown 时直达调用。〔HJ-AGI 实测:M3 把点击塞pendingClicks、在update的 play 分支(menu early-return 之后)才消费 → 菜单点击永不处理 → 启动不了;收归 L1 结构性根除。check 会拦getInput与"零输入方法"。〕 - ⚠️ 驱动插件 onFrame:
update(dt)内必须bundle.tick(dt)(bundle是 createGame 入参)——否则 timer-scheduler/gamefeel 等的 onFrame 不推进、timer 永不到期、一局不结束。 - viewport:画面口径读入参
viewport.w/h,别硬编码 390×844。 - 取证面(
_forensicsView()必须实现,可测性红线):实例必须挂_forensicsView()→{ state(), measures() };state()导出可观测态——至少{ phase, score },有可点目标则加targets:[{x,y,occupied}](九门据此验「真活+有进展」+ 驱动)。targets 要反映「当前该点什么才推进」:多步动作(如选客→选货)在选中后,把第二步目标(货架商品)也标occupied:true—— 否则零-LLM driver 单击跟不动两步、H 门验不到进展(实测 wanglanmei-v4:两步成单 → driver 只选客不成单 → score 0 误判)。最稳:核心成单做成单击即完成。不写则 check 拦 + 九门 E_live/H_progress 读不到状态。另可选挂_handleAction(action)(host.do() 走逻辑驱动,便于确定性 e2e)。范式见_template/src/game-logic.js。 - 关键事件记日志(
ctx.log):在 init/场景切换/得分/出错处 调ctx.log('tag', 信息)(ctx=init(boot).boot.ctx)→ 写window.__gameLog,便于读日志诊断运行时问题。插件调用已由 host 自动记(你只补游戏语义事件)。
4. 11 注入插件能力速查(调 API,别重写;另 runtime-probe 仅取证不注入)
完整签名以
game-runtime/src/plugins/<name>/api.d.ts为准(本表只给「选型 + 头部调用」)。每件都是createXxxPlugin(opts)工厂,经 host-config 实例化、注入。命名全 engine 级、零玩法语义。
基元层(8 件 · 算法/物理/手感/音/存档)
| 插件 | 用于 | 头部 API |
|---|---|---|
| collision | 碰撞查询(纯数学) | collision.circleVsCircle/circleVsAabb/aabbVsAabb/satVsPolygon→{hit,depth,nx,ny};raycast*;SpatialHash(#3 都挂实例)。Aabb 盒 #12:可传 {x,y,w,h}(左上角+尺寸,与 Rect/drawButton 一致)或 {x,y,hw,hh}(中心+半宽高)——两形态都对;Circle {x,y,r} |
| physics-lite | 街机运动原语(非刚体) | Projectile{step,reset}/integrateProjectile(state,dt,opts);springStep/criticalDamping;moveWithConstraint;clampSpeed/applyFriction。⚠️ ProjectileState={px,py,vx,vy}(位置是 px/py 非 x/y);重力经 opts.gravity={x,y}` |
| gamefeel | 手感 | easing(13 曲线 in/out/inOut);内置 gamefeel.inputBuffer{consume,peek};工厂(免 new + 自动注入受控 time,#4):gamefeel.createCoyoteTimer(opts)→{setGrounded,isAvailable,consume}、gamefeel.createComboWindow(opts)→{hit,getCount} |
| particles-juice | 粒子 + 打击感 | spawnEmitter(preset|cfg,x,y)(或 burst(x,y) 直发 burst 预设,#6/#10 母语别名);hitStop(dur)/getTimeScale;shakeScreen/getShakeOffset;flashScreen/pulseScale;PRESETS(burst/trail/drift)。⚠️ juice.render(g) 必须在你的 render(g) 里每帧手调(非自动,#11)——忘了 → 粒子/打击感不显示;step(dt) 默认 autoStep 自驱(经 onFrame),要手调 step 须工厂传 autoStep:false 防双步进 |
| palette-post | 调色 + 后处理 | rgbToHsl/hslToRgb/applyPaletteMap/shiftHsl;setPost/renderPost(vignette/dither/scanline)或 applyPreset('retro'|'crt'|'soft'|'dither'|'none') 一键观感(#14)。换色色板(from/to)仍由你注入、插件不内置成品色 |
| audio-music | 程序化音频 | loadSong(载曲)/play(intensity?)(播已载曲;入参是情绪强度 0..1、不是曲目名,#9)/stop/loop;setIntensity;playSfx(name)(#8 放开):预设 blip/thud/chime 或语义名 click/hit/win/coin/jump/fail 等(自动映射、未知兜底 blip) |
| save-progress | KV 持久化 | get(key,default)/set/remove/clear;createMemoryAdapter/createLocalStorageAdapter |
| runtime-probe | 启动取证(六锚) | mark(anchor)/getRecords/toJSONL/verify/verifyChain |
编排层(4 件 · 让你"只写玩法、不手写编排")
| 插件 | 用于 | 头部 API |
|---|---|---|
| session-score | 计分 + 胜负闸 | addScore(d)(=add(d) 别名)/setScore/getScore/getBest/reset;win()/lose()/getOutcome()/isOver()。胜负条件你写:if(lives<=0) s.lose()。best 接 save:createSessionScorePlugin({best:{load:()=>save.get('best',0),save:b=>save.set('best',b)}}) |
| hud-ui | HUD/UI 绘制 + 命中 | 纯:pointInRect/pointInCircle/layoutRow/layoutColumn/hitTest;绘制(传 g):measureText/drawText/drawPanel/drawButton/drawBar(g,...) |
| scene-fsm | 场景/状态机 | define(name,{onEnter,onExit,update,render})/start(name)/transition(to,payload?)/current()/update(dt)/render(g)/reset()。状态名你定 |
| timer-scheduler | 受控时钟调度 | after(ms,cb)/every(ms,cb)/sequence([{delayMs,run}])/cancel(h)/clear。⚠️ 单位是毫秒 #7:3 秒写 after(3000,cb) 不是 after(3,cb)、every(1000,cb)=每秒(与 update(dt) 的 dt=秒相反,别混)。经 onFrame+受控时钟(游戏须在 update 内 bundle.tick(dt) 驱动,见 §3),不要用 setTimeout |
典型组合:菜单→玩→结算 用 scene-fsm;分数与最高分用 session-score(best 接 save-progress);倒计时/刷新波次用 timer-scheduler;分数/按钮/血条画面用 hud-ui;手感打击用 particles-juice + gamefeel。
⚠️ M3 实测易犯的幻觉 API(❌别这么写 → ✅正确)
铁律:插件方法名一律以
src/plugins/<name>/api.d.ts为准,别按"常见库套路"臆测。 以下是 M3 即便读了 api.d.ts 仍反复臆测出的错名(HJ-AGI-003 saae 实测),逐条记牢:
- ❌
sceneFsm.defineScenes({...})→ ✅sceneFsm.define(name, {onEnter,onExit,update,render})逐个定义(没有defineScenes)。 - ❌
sceneFsm.define({ menu:{...}, play:{...}, over:{...} })单对象批量注册 → ✅ 每个场景各调一次sceneFsm.define('menu', {...}); sceneFsm.define('play', {...}); sceneFsm.define('over', {...})(define(name, handlers)第一参恒为场景名字符串;传对象 → 第一参变成[object Object]、menu/play/over 全没注册 →transition('play')找不到目标态被忽略 → 卡菜单进不去 play。实测合成/2048 类翻车点)。可链式.define(...).define(...)。回调表里update(dt)只收 dt、render(g)只收 g、与五法同形——别臆造场景名/state 首参(#15),绝不写update(scene, dt)。 - ✅
hudUi.drawButton(g, rect, opts)回吐它绘制的命中矩形{x,y,w,h}(阶段一B #5 改签名):直觉写法就对——const btn = hudUi.drawButton(g, {x,y,w,h}, {label}); if (hudUi.pointInRect(px,py,btn)) {...},drawButton 返回命中矩形、直接接住判命中即可(也可先单独定义 rect 再画再 pointInRect,两种都对)。命中判定hudUi.pointInRect(px, py, rect)(签名(px, py, rect);只在 hudUi,collision 插件没有 pointInRect 方法)。另:drawButton第二参是 rect 对象{x,y,w,h}、不是 label/坐标位置参——别写drawButton(g, '开始', x, y, w, h)。 - ✅ 起局/交互按钮的命中矩形在场景
onEnter(状态进入点)确定性建立、render只画不写——绝不让它只在render副作用里生成(menuStartBtn = renderMenu(...)那种回写):并发渲染帧饥饿时render可能一次都没跑过 → 命中矩形恒null→ 菜单点不动、一局起不来(卡 menu 头号成因)。把按钮几何抽成纯函数,onEnter调它设命中区、render调它画,命中与绘制同源不漂移(_template*六款范例的menuStartRect/overRestartRect即此写法)。 - ❌ scene 的 update 回调臆造首参写成
update(running, dt)→ ✅ scene 的update回调只收一个参数(dt)(sceneFsm.define(name, { update(dt){…} });sceneFsm.update(dt)只透传 dt,没有 running 之类首参)。写错首参 → 真正的 dt 落到第二参=undefined →state.elapsedMs += undefined = NaN→ 不再 spawn/计时、计时显示「NaNs」、游戏空转看着像没 bug 实则永不推进(实测 wanglanmei-v2:菜单能进 play 但无顾客、营业额恒 0)。任何回调签名都以src/plugins/<name>/api.d.ts为准,别臆造参数。 - ❌
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()或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 harnesswrite_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 只见扁平件,结构性根除。〕
5. 资产:美术/音乐/音效放哪 + 改图去哪找
- 放置:全部进
assets/(.jpg美术、.mp3音乐音效),并在assets/manifest.json登记每件file/role/bytes/sha256/source{tool,model,prompt}。 - 声明:
src/assets.js用 键名间接引用——IMAGE_FILES = { itemSoda: 'item-soda.jpg', ... }+ 业务 id→键映射。代码里用assets.images.itemSoda,不写死文件名。 - 加载:
game.js的init里await loadAssets('./assets/');并行new Image(),单图失败 → 该键置 null + 记 missing,游戏照常启动(render 走程序化/占位回退,绝不因缺图崩)。音频在首次手势后由 audio-director 懒加载解码。 - 改一张图(LLM 怎么找):① 查
assets/manifest.json找到该 role 的行(含原 prompt/hash);②mmx重新生成,覆盖同名文件;③ 更新 manifest 该行sha256/bytes/prompt;④ 文件名没变 →assets.js/render.js都不用动;新增品类才往IMAGE_FILES加键。
6. 用 mmx 生成素材
mmx = MiniMax 官方多模态 CLI(本机已装;创始人 2026-06-12 拍板为美术/音乐现行方案,免 GPU)。直接 CLI 调,产物存 assets/ + 登记 manifest:
# 美术(模型 image-01)
mmx image generate --prompt "<主体> + 统一风格词 + 纯浅奶油背景,居中,简洁" --out item-soda.jpg
# 音乐(模型 music-2.6-free,对 API key 不限量)
mmx music generate --prompt "<情绪/配器/场景>" --instrumental --bpm 92 --out bgm-idle.mp3
# 音效经 speech 合成;视频 video generate(异步)
铁律:全套素材共用一组「风格词」(美术统一性之根,见 wanglanmei-ref manifest 的 styleWords);每次调用都把 prompt 写进 manifest/gen-ledger,便于复现与替换。参考成本:一款游戏 ~15 件素材 ≈ ¥0.5。
注:mmx 走公网,认证靠
~/.mmx/credentials.json(非内网 key)。后端「生成触发 + 消费侧自动装配」产线尚在建(engine-E2/E3);当前 MVP 阶段 CLI 手动产素材 已足够。
7. 红线清单(违任一条 = 不合格)
- 游戏工厂
game.js零引擎 import;引擎 import 只活entry.js。 - 游戏代码零
Date.now/performance.now/Math.random/setTimeout/setInterval/requestAnimationFrame/addEventListener/new AudioContext——全经boot.ctx或插件。 - 通用机制能调插件就别自己写;反过来,玩法语义(规则/数值/胜负条件/状态名/美术)绝不塞进插件——插件是 engine 级、零品类词。
render(g)画在 host 注入的g(mainContext);不自取 canvas。- 资产缺失必须容错降级,绝不连坐崩溃。
core.js/balance.js保持纯净(无 I/O、无引擎),可 node 单测。- index.html 必须自适应缩放 canvas(用 _template 那份:
#game { width: min(100vw, calc(100vh*W/H)); height: min(100vh, calc(100vw*H/W)); ... !important }+#wrapflex 居中)。别回退到固定390×844+overflow:hidden——桌面/矮窗口会把画布底部(篮子/按钮/角色)裁在视区外且无法滚动(真人试玩才暴露,headless 测不到)。!important是为覆盖 host 在 stub 通道写的内联 display 尺寸;buffer 不变(evidence 像素哈希不受影响),输入经boot-game-host.toCanvasXY按getBoundingClientRect归一,缩放下点击坐标仍准。
8. 自测(完成前必跑)
- 纯逻辑:
node --test games/<game>/test/*.test.mjs(core/balance 的规则、边界、胜负闸)。 - 真玩取证(九门):e2e CDP harness 跑
A_boot/B_uncaught/C_frame/D_render/E_live(build-health 客观门)+F_wiring/G_input/H_progress/I_control(driver 门);四件套证据(截图/probe/trajectory/checklist)。详见 skillgame-e2e-cdp-harness与cheap-model-game-generation。 - 结构门:工厂独立默认导出、零引擎 import(import-graph 测);插件只调公开 API、不直透 littlejsengine。
相关 skill / 契约
- 插件库总览与受控面 →
game-runtime/src/core/api.d.ts(PluginContext 6 项 + getEngine)、各plugins/<name>/api.d.ts - 装载契约 →
game-runtime/src/core/game-host.d.ts(GameInstance 五法 + GameHostFactory) - 契约匹配范例 →
game-runtime/games/_template/(克隆起点 + 照它改;签名以现契约为准)。wanglanmei-ref/是旧 opts 签名漂移的进阶参照,只看其规模/styleWords,别照它的 game-logic 写 - 便宜模型造游戏 worker loop / 九门 → skill
cheap-model-game-generation、game-e2e-cdp-harness - 渠道发行/打包 → skill
runtime-and-multichannel