miaoda-game-devkit
v0.10.0
Published
Shared React and Phaser game lint plus deterministic testing tools for Miaoda games
Readme
miaoda-game-devkit
妙搭 Phaser 4 与 React 游戏模板的共享开发基础设施。
该开发工具包提供:
- React 游戏的生产/手动时钟、Strict Mode 安全 Controller ownership、JSDOM
Vitest 配置和自动收集运行错误的 Testing Library setup,分别从
miaoda-game-devkit/react、miaoda-game-devkit/react/testing和miaoda-game-devkit/react/vitest-config导入; - React 的
playthroughTest与项目级 reporter 自动验证连续的 enter/milestone/finish 玩法阶段、真实 DOM 输入或确定性推进、有界结果、结果断言和阶段状态变化;聚焦运行只提示未检查, 完整运行缺少有效流程时失败;
playthroughTest 的主流程回调只需要编排 enter、至少三个唯一命名的 milestone 和
finish。enter 必须声明真实输入 act;后续阶段声明 act 或确定性 step。每个阶段还要
提供初始为 false 的有界 until,以及使用 test-bound expect 的 assert。DevKit 在验证阶段
前后 DOM 或权威 observe 状态确实变化后自动提交阶段证据。每个 milestone 使用非空且
唯一的领域名称,表示一段独立的玩法进展。finish.kind 对有限结果使用 terminal,对无尽或
沙盒游戏的有意义进展使用 progress。大型游戏保留一条最短关键流程;分支、关卡规则和恢复
清理由专门测试覆盖。不要再次调用 userEvent.setup()。
Playthrough 测试始终以非并发任务运行,且只统计当前阶段 act 执行窗口内的输入事件,避免
同一 JSDOM 中其他测试或后台事件污染玩法证据。
时间、帧或自定义 scheduler 流程必须显式传入确定性 step;精确物理时间、暂停恢复
或调度清理测试可使用 ManualGameClock。Canvas/Controller 流程在 playthroughTest 上声明一次
observe,返回 JSON 可序列化的权威 Telemetry 状态;框架自动比较流程状态并复用为超时诊断。
失败的 production playthrough 会额外输出一条有界 TRACE::记录已完成 milestone、当前阶段、
阶段前后可观测状态与推进步数。通过流程不输出 TRACE,避免增加
常规 CI 噪声。显式启用 Vitest retry 时,每次尝试会重置证据,最终报告只展示最后一次尝试的
CAUSE 与 TRACE;repeat 同样为每轮建立新证据。测试超时或取消会中止后续有界推进,并通过
Vitest 的 test-local failure hook 在 worker 最终同步前保留活动阶段;若测试主体已完成、随后由
afterEach 或运行时错误判失败,reporter 仍会从 task metadata 恢复这条流程证据。状态优先来自 observe,没有 Telemetry 时才使用有界 DOM
摘要;不要把 TRACE 当作新的断言或第二份业务状态。
Vitest 的 fake timers(包括 vi.advanceTimersToNextFrame())适合在 devkit 自身的
专门 scheduler 合约中使用,但不在所有 production playthrough 中全局启用;React
Testing Library 的 asyncWrapper 和 user-event 内部 timer 会因此互相影响。使用自定义
scheduler、Worker 或未通过浏览器 timer 接入的引擎时,应传显式 step 并通过
diagnostics 暴露权威状态;已声明 observe 的流程无需重复提供 diagnostics。
React Vitest 在项目声明 Phaser 3 时使用其 browser bundle,并提供仅供模块能力探测的 最小 Canvas 2D 边界,使 Phaser 3 代码不会阻断普通 JSDOM 测试;Storage 使用 JSDOM 按 origin 提供的原生实现,Node 25+ worker 会关闭冲突的 Node Web Storage, 并在每个用例后清理;该 Canvas 适配不执行真实绘制, 也不提供 Canvas/WebGL 像素证明;
React Vitest 统一统计
src/game/core/与src/game/runtime/的逐文件覆盖率;Core 使用 50/40 分支基线,Runtime 使用 40/30 分支基线。模板通过pnpm test启用 V8 coverage,项目不能覆盖范围或降低阈值;支持 DOM 鼠标、键盘和触摸输入的确定性 Phaser 无头测试宿主;
不依赖渲染器的运行时、交互区域和文字健康检查;
只在 devkit 自身 CI 中运行、不会发布到业务项目的通用 Vitest 契约测试;
显式隔离的
miaoda-react-game-lint与miaoda-phaser-game-lint命令, 共享 TypeScript、Biome、Tailwind 和 Oxlint 检查,同时分别校验 React 与 Phaser 4 配置边界。统一的
defineGameVitestConfig配置工厂,固定 Phaser 无头测试依赖的基础配置, 同时允许每个游戏声明 Scene 审计范围,并配置额外 alias、setup 文件和超时。与 Vite
defineConfig同签名的defineGameViteConfig配置工厂,统一维护生产 Scene 的运行时基础能力,同时完整保留游戏自己的 Vite 配置。
互动游戏的长流程优先使用游戏自己暴露的 globalThis.gameTelemetry。稳定 façade 通常调用
installGameTelemetry(this.game, telemetry) 绑定 Game 生命周期;只有刻意局部的 façade 才绑定
Scene 生命周期。开发工具包只规定最小
外壳:version: 1、read.session() 和游戏自有的 controls;fixtures 可选。快照字段、
控制方法名和 Core 适配由游戏负责,controls 必须调用与生产输入共享的权威命令。Telemetry
不会复制 host.stepFrames()、DOM 输入或 Scene 生命周期 API。
createHeadlessGame 分别记录测试请求和 Phaser 公开事件确认的执行结果。输入必须被
InputPlugin 消费,完整帧必须由活动 Scene 收到 UPDATE,Arcade 单步必须产生
WORLD_STEP,Scene 转场和 restart 必须进入目标生命周期,才能满足对应的
host.assertGameplayEvidence() 门禁。业务 checkpoint 和 authoritative state 仍必须由
测试显式断言,devkit 不从引擎事件推断玩法语义。
HEADLESS host 会把 Phaser 自身的 warning/error、Loader 文件失败、漏掉 load.start() 的队列、
未注册 Scene 命令和可见对象的 __MISSING 纹理升级为测试失败。只有经过确认的引擎
warning 才能在创建 host 时通过 ignoreEngineWarnings: [/pattern/] 显式豁免;资源 URL
仍须通过浏览器验证,因为 JSDOM 图片加载只支持测试用的 data:image/ URL。刻意暂存
资源供稍后加载的 Scene 可以通过 allowIdleLoaderQueues: ["SceneKey"] 精确声明。
玩法测试应优先使用 gameplayTest。它是玩法 contract 的唯一权威声明,在执行阶段绑定
HEADLESS host,并在 onTestFinished 校验 Ledger。vitest.config.ts 默认只列生产 Scene,
不再复制 contract id、测试文件和证据要求。Reporter 从测试 metadata 汇总交互项目的最低
基线、失败、证据不足、Scene 未访问和 registry 不一致。旧项目仍可提供显式
gameplayAudit.contracts 清单,但新游戏通常不需要。
两套模板直接调用各自的命令行程序:
{
"scripts": {
"lint": "miaoda-react-game-lint"
}
}Phaser 4 模板对应使用 miaoda-phaser-game-lint。命令类型不通过依赖猜测,因此 React
模板即使预装 Phaser 3 及相关插件,也不会解析 Phaser 或运行 Phaser 4 检查。旧的
miaoda-game-lint 不再发布,避免在同时包含 React 与 Phaser 依赖的项目中产生歧义。
可编辑的游戏机制源码
Devkit 发布统一的 miaoda 命令。裸 miaoda-game-* 包名默认从公开源码索引解析为
当前稳定版本,Devkit 直接下载并校验该包及其传递机制依赖的源码归档;消费项目自己的 pnpm
10 或 11 只负责最终 workspace 联动和普通第三方依赖安装。生产 TypeScript 源码写入
src/game-mechanics/<包名>/。.miaoda/mechanics-source.json 只保存版本、索引地址与安装摘要,
不保存第二份源码。
pnpm exec miaoda mechanics --help
pnpm exec miaoda mechanics list
pnpm exec miaoda mechanics list --domain=grid --engine=react
pnpm exec miaoda mechanics add miaoda-game-beam-core
pnpm exec miaoda mechanics add [email protected] \
--source-index=https://public.example.com/game-mechanics/stable.json
pnpm exec miaoda mechanics statuslist 只列出稳定索引中实际可安装的包,并从 Devkit 自带的 capabilities JSON 动态汇总
domain 的 owns 能力;带筛选时会显示包负责和不负责的边界、使用指引及可测试性。它不会安装
源码,也不会维护另一份容易过时的文字目录。先选择覆盖需求的最小包集合,再单独运行 add,
成功后阅读生成的 src/game-mechanics/README.md。
默认索引是 https://resource-static.bj.bcebos.com/miaoda-game/stable.json。只有调试、测试或
私有镜像场景才需要通过 --source-index 或 MIAODA_MECHANICS_INDEX_URL 覆盖。
重复添加同版本不会覆盖源码;status 报告 clean、modified 或 missing。更新到不同版本时,
若本地源码已修改,命令会在写入前失败。包内的单元测试、Vitest 配置、构建输出和 node_modules
不会复制进游戏项目,避免它们进入游戏自身的 TypeScript 编译范围。索引中的相对 TGZ URL 以
stable.json 为基准解析;每个版本必须提供精确的机制依赖和 SHA-256。机制源码只通过该索引
解析,不回退 npm registry,也不接受绕过索引的直接 TGZ 或本地包路径。
开发工具包自行维护并精确固定 Vitest 等工具版本,从自身依赖中解析可执行文件,使用方不需要重复声明
Biome、Oxlint 或 tsgo。仓库中保留稳定的命令入口,因此 dist/ 尚未生成时,pnpm
也能正确建立命令链接;工作区开发状态下由 Nx 在模板检查前构建开发工具包,发布包则
直接携带运行时 dist/。打包消费者契约会验证固定 Vitest 版本以及 reporter 的通过、聚焦、
缺失流程、retry 失败诊断和 hook 失败诊断行为。开发工具包内部的 *.test.* 与构建后的
*.contract.mjs 都不会
进入 npm 包;这些契约只由 devkit 自己的测试目标执行。
monorepo 中的模板可以放置只包含 extends: ["miaoda-game-devkit/biome"] 的薄配置,
用于让 Biome 从模板 package.json 判断依赖声明。规则本身仍由本开发工具包维护;生成后的
独立模板位于仓库根目录,不需要这个 workspace 定位配置。
模板的 tsconfig.json 应继承 miaoda-game-devkit/tsconfig-base,只保留源码范围和路径
别名等项目差异。TypeScript 严格模式、bundler 模块解析和无输出检查等平台基线由开发
工具包集中维护,两套模板专用 lint CLI 会直接检查模板的这个配置。
Phaser4 模板的 vite.config.ts 应从 miaoda-game-devkit/vite 导入 defineGameViteConfig,调用方式
与 Vite 的 defineConfig 相同,支持配置对象、Promise、同步配置函数和异步配置函数:
export default defineGameViteConfig({
plugins: [miaodaDevPlugin()],
});该入口只增强 src/scenes 中对 Phaser 的导入。游戏 Scene 仍按标准方式继承
Phaser.Scene;devkit 在 Scene 创建前提供 RexUI,并在创建后验证业务确实完成了 RexUI
根布局。RexUI 的加载、Phaser 4.0 兼容和 Scene Plugin 生命周期属于 devkit 责任,游戏
配置不重复注册。
具体游戏的玩法测试仍由模板使用方放在 tests/。运行时测试从本开发工具包
导入 createHeadlessGame;有限目标游戏通过合法 Telemetry controls 走到正常成功终局,
无尽或沙盒游戏证明有意义的持续进展。短输入测试从入口操作走到一个主要操作,用真实 DOM
事件证明生产输入接线,并通过 Telemetry 断言业务状态变化。
同步帧逻辑使用 stepFrames();update() 返回 Promise 时使用
await stepFramesAsync(),其他同步操作启动生命周期 Promise 后使用 await settle()。
输入驱动会把 DOM 兼容事件发送到 Phaser 实际配置的输入目标,并在每次事件后检查异步 错误和运行时健康状态。坐标参数使用 Phaser 游戏坐标:
host.stepFrames();
await host.input.mouse.click(100, 80);
await host.input.keyboard.down(Phaser.Input.Keyboard.KeyCodes.RIGHT);
host.stepFrames(10);
await host.input.keyboard.up(Phaser.Input.Keyboard.KeyCodes.RIGHT);
await host.input.touch.start(40, 40);
await host.input.touch.move(100, 80);
await host.input.touch.end(100, 80);click、press 和 tap 适合离散命令。需要验证按住、拖动或滑动时,应分别调用开始和
结束方法,并在两者之间推进完整 Phaser 帧。JSDOM 没有布局和真实渲染器;这些驱动验证
DOM 到 Phaser 的输入、命中测试和游戏状态变化,不验证 Canvas/WebGL 像素、CSS 布局、
浏览器焦点或真实触摸策略。
模板的 vitest.config.ts 应从 miaoda-game-devkit/vitest-config 导入
defineGameVitestConfig。JSDOM 环境、Phaser 浏览器构建别名、通用 setup 和 mock 隔离
以及 Scene coverage 均由开发工具包统一维护。模板必须传入 projectRoot 和
gameplayAudit;默认只列生产 Scene,关键流程及证据要求由 gameplayTest 自己声明一次。
确有需要时,可以通过 aliases、additionalSetupFiles、testTimeout 和 hookTimeout
增加安全的项目配置,不能覆盖测试范围、运行环境、审计 reporter 或 coverage 基线。
gameplayTest 会自动要求访问其声明的 Scene 并完成 host 销毁。玩法进展、恢复和终局应在
断言 authoritative state 后调用 host.checkpoint("progress")、
host.checkpoint("recovery") 等稳定检查点,并在对应 gameplayTest 中通过 requireCheckpoint 声明。
运行结束会输出稳定的 GAMEPLAY_AUDIT: ALL CONTRACTS PASSED 或
GAMEPLAY_AUDIT: ... FAILED 标识。该标识只表示玩法证据审计;pnpm test 的整体结果
必须以最后的 TEST_RESULT: PASS 或 TEST_RESULT: FAIL 为准。失败报告按根因合并问题并
给出直接修复步骤;测试文件无法加载或配置要求的测试尚未建立时,不再继续报告 Scene
注册等派生问题。先处理第一类错误,再重新运行 pnpm test。
Phaser Vitest 配置使用 minimal reporter;React Vitest 配置使用无 ANSI 的精简 reporter。
两者都保留失败定位、coverage 摘要和最终机器标识,同时避免输出完整 DOM 快照、测试列表和逐文件噪声。
发布顺序
- 在开发工具包中运行类型检查和测试。
pnpm test会额外生成经过clean-publish清理的 tarball,在隔离项目中按发布目录结构解包,并以NODE_ENV=production运行真实 React DOM 输入测试。 - 使用
pnpm run release:dry-run检查将要发布的文件和清理后的package.json。 - 根据版本类型运行
pnpm run release:patch、release:minor或release:major。 发布脚本会先测试和构建,再通过clean-publish从临时目录发布;发布包会移除devDependencies和开发脚本,不会修改工作区中的原始package.json。 - 也可以使用
pnpm run release发布已经手动设置好版本的开发工具包。 - 最后运行模板的
pnpm run build:template,让output/使用已发布版本重新生成锁文件。
pnpm-workspace.yaml 已将 miaoda-game-devkit 加入 minimumReleaseAgeExclude,
因此新版本发布后无需等待 24 小时即可生成模板锁文件。--skip-install 仅用于发布前
检查 output/ 的产物边界,不能用于正式 CI 构建。
