miaoda-game-devkit 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,21 +10,25 @@
10
10
  - 统一的 `miaoda-game-lint` 命令,聚合 TypeScript、Biome、Tailwind、
11
11
  Oxlint 和 Phaser 通用契约检查。
12
12
  - 统一的 `defineGameVitestConfig` 配置工厂,固定 Phaser 无头测试依赖的基础配置,
13
- 同时允许每个游戏声明玩法审计清单,并配置额外 alias、setup 文件和超时。
13
+ 同时允许每个游戏声明 Scene 审计范围,并配置额外 alias、setup 文件和超时。
14
+
15
+ 互动游戏的长流程优先使用游戏自己暴露的 `globalThis.gameTelemetry`。稳定 façade 通常调用
16
+ `installGameTelemetry(this.game, telemetry)` 绑定 Game 生命周期;只有刻意局部的 façade 才绑定
17
+ Scene 生命周期。开发工具包只规定最小
18
+ 外壳:`version: 1`、`read.session()` 和游戏自有的 `controls`;`fixtures` 可选。快照字段、
19
+ 控制方法名和 Core 适配由游戏负责,controls 必须调用与生产输入共享的权威命令。Telemetry
20
+ 不会复制 `host.stepFrames()`、DOM 输入或 Scene 生命周期 API。
14
21
 
15
22
  `createHeadlessGame` 还记录测试期间真实发生的输入、完整帧、物理步、Scene 注册与访问、
16
23
  Scene 命令、restart、业务 checkpoint 和销毁。玩法测试可以调用
17
24
  `host.assertGameplayEvidence()` 把这些客观事实设为门禁;它不推断业务语义,
18
25
  authoritative state 仍必须由测试显式断言。
19
26
 
20
- 玩法测试应优先使用 `gameplayTest`。它在 Vitest 收集阶段写入测试声明,
21
- 在执行阶段绑定 HEADLESS host,并在 `onTestFinished` 校验 Ledger。配置加载阶段会先校验
22
- 交互项目的最低基线:至少一个 `kind: "playthrough"` contract 必须要求真实输入、完整帧和
23
- 语义 checkpoint,每个非入口 Scene 必须有生产转场覆盖;检测到 `scene.restart()` 时还必须
24
- 声明 `flows.restart: true` 和 `kind: "recovery"` contract。配置预检失败时测试和 coverage
25
- 都不会运行。配置通过后,`gameplayAudit` reporter 才会检查测试声明,并在结束时汇总
26
- 缺失、失败、证据不足、Scene 未访问和 registry 不一致。`registerGameplayContract` 保留为
27
- 不参加执行前预检的低层接口。
27
+ 玩法测试应优先使用 `gameplayTest`。它是玩法 contract 的唯一权威声明,在执行阶段绑定
28
+ HEADLESS host,并在 `onTestFinished` 校验 Ledger。`vitest.config.ts` 默认只列生产 Scene,
29
+ 不再复制 contract id、测试文件和证据要求。Reporter 从测试 metadata 汇总交互项目的最低
30
+ 基线、失败、证据不足、Scene 未访问和 registry 不一致。旧项目仍可提供显式
31
+ `gameplayAudit.contracts` 清单,但新游戏通常不需要。
28
32
 
29
33
  模板直接调用开发工具包提供的命令行程序:
30
34
 
@@ -51,7 +55,9 @@ monorepo 中的模板可以放置只包含 `extends: ["miaoda-game-devkit/biome"
51
55
  工具包集中维护,`miaoda-game-lint` 会直接检查模板的这个配置。
52
56
 
53
57
  具体游戏的玩法测试仍由模板使用方放在 `tests/`。运行时测试从本开发工具包
54
- 导入 `createHeadlessGame`,通过真实 DOM 输入驱动游戏,并断言有意义的业务状态变化。
58
+ 导入 `createHeadlessGame`;有限目标游戏通过合法 Telemetry controls 走到正常成功终局,
59
+ 无尽或沙盒游戏证明有意义的持续进展。短输入测试从入口操作走到一个主要操作,用真实 DOM
60
+ 事件证明生产输入接线,并通过 Telemetry 断言业务状态变化。
55
61
  同步帧逻辑使用 `stepFrames()`;`update()` 返回 Promise 时使用
56
62
  `await stepFramesAsync()`,其他同步操作启动生命周期 Promise 后使用 `await settle()`。
57
63
 
@@ -79,13 +85,13 @@ DOM 到 Phaser 的输入、命中测试和游戏状态变化,不验证 Canvas/
79
85
  模板的 `vitest.config.ts` 应从 `miaoda-game-devkit/vitest-config` 导入
80
86
  `defineGameVitestConfig`。JSDOM 环境、Phaser 浏览器构建别名、通用 setup 和 mock 隔离
81
87
  以及 Scene coverage 均由开发工具包统一维护。模板必须传入 `projectRoot` 和
82
- `gameplayAudit`;清单列出生产 Scene、关键 contract、负责的测试文件和证据要求。
88
+ `gameplayAudit`;默认只列生产 Scene,关键流程及证据要求由 `gameplayTest` 自己声明一次。
83
89
  确有需要时,可以通过 `aliases`、`additionalSetupFiles`、`testTimeout` 和 `hookTimeout`
84
90
  增加安全的项目配置,不能覆盖测试范围、运行环境、审计 reporter 或 coverage 基线。
85
91
 
86
92
  `gameplayTest` 会自动要求访问其声明的 Scene 并完成 host 销毁。玩法进展、恢复和终局应在
87
93
  断言 authoritative state 后调用 `host.checkpoint("progress")`、
88
- `host.checkpoint("recovery")` 等稳定检查点,并在清单中通过 `requireCheckpoint` 声明。
94
+ `host.checkpoint("recovery")` 等稳定检查点,并在对应 `gameplayTest` 中通过 `requireCheckpoint` 声明。
89
95
  运行结束会输出稳定的 `GAMEPLAY_AUDIT: ALL CONTRACTS PASSED` 或
90
96
  `GAMEPLAY_AUDIT: ... FAILED` 标识。该标识只表示玩法证据审计;`pnpm test` 的整体结果
91
97
  必须以最后的 `TEST_RESULT: PASS` 或 `TEST_RESULT: FAIL` 为准。失败报告按根因合并问题并
@@ -186,8 +186,8 @@ interface GameplayAuditOptions {
186
186
  };
187
187
  /** 生产 Scene registry 中预期出现的全部 Scene key。 */
188
188
  scenes: string[];
189
- /** 必须定义、执行并满足证据要求的玩法契约。 */
190
- contracts: GameplayAuditContract[];
189
+ /** 可选的兼容清单;省略时以 gameplayTest metadata 作为唯一权威来源。 */
190
+ contracts?: GameplayAuditContract[];
191
191
  }
192
192
  /** 通过 Vitest task metadata 在 worker 与 reporter 之间传递的契约数据。 */
193
193
  interface GameplayContractMetadata {
@@ -186,8 +186,8 @@ interface GameplayAuditOptions {
186
186
  };
187
187
  /** 生产 Scene registry 中预期出现的全部 Scene key。 */
188
188
  scenes: string[];
189
- /** 必须定义、执行并满足证据要求的玩法契约。 */
190
- contracts: GameplayAuditContract[];
189
+ /** 可选的兼容清单;省略时以 gameplayTest metadata 作为唯一权威来源。 */
190
+ contracts?: GameplayAuditContract[];
191
191
  }
192
192
  /** 通过 Vitest task metadata 在 worker 与 reporter 之间传递的契约数据。 */
193
193
  interface GameplayContractMetadata {
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-CusQfLYQ.mjs';
2
- export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-CusQfLYQ.mjs';
1
+ import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-CCJo4Qhk.mjs';
2
+ export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-CCJo4Qhk.mjs';
3
3
  import * as Phaser from 'phaser';
4
4
  import { TestContext, TestOptions } from 'vitest';
5
5
 
@@ -68,6 +68,29 @@ declare module "vitest" {
68
68
  }
69
69
  }
70
70
 
71
+ /** The single global entry point used by game-owned HEADLESS telemetry. */
72
+ declare const GAME_TELEMETRY_GLOBAL: "gameTelemetry";
73
+ /**
74
+ * A game-owned observation and command boundary for deterministic runtime tests.
75
+ * Snapshot fields and command names intentionally remain specific to each game.
76
+ */
77
+ interface GameTelemetry<SessionSnapshot, Controls extends object, Fixtures extends object = Record<string, never>> {
78
+ version: 1;
79
+ read: {
80
+ session(): SessionSnapshot;
81
+ };
82
+ controls: Controls;
83
+ fixtures?: Fixtures;
84
+ }
85
+ type AnyGameTelemetry = GameTelemetry<unknown, object, object>;
86
+ /**
87
+ * Install a game- or Scene-owned Telemetry handle and remove it on lifecycle exit.
88
+ * Game ownership survives Scene transitions; Scene ownership is useful for local facades.
89
+ */
90
+ declare function installGameTelemetry<Telemetry extends AnyGameTelemetry>(owner: Phaser.Game | Phaser.Scene, telemetry: Telemetry): () => void;
91
+ /** Read the current typed game handle or fail with a direct setup diagnosis. */
92
+ declare function getGameTelemetry<Telemetry extends AnyGameTelemetry>(): Telemetry;
93
+
71
94
  declare function assertSceneRuntimeHealth(scene: Phaser.Scene): void;
72
95
  declare function assertSceneInteractiveHealth(scene: Phaser.Scene): void;
73
96
 
@@ -76,4 +99,4 @@ declare function collectMissingBitmapGlyphs(text: string, chars: Record<number,
76
99
  /** 在文字状态损坏时抛出适合 CI 和编码智能体处理的错误。 */
77
100
  declare function assertSceneTextHealth(scene: Phaser.Scene): void;
78
101
 
79
- export { type GameplayContractHandle, GameplayContractMetadata, type GameplayContractOptions, type GameplayTestArguments, type GameplayTestContext, type GameplayTestOptions, HeadlessGameHost, HeadlessGameplayEvidenceRequirements, assertSceneInteractiveHealth, assertSceneRuntimeHealth, assertSceneTextHealth, collectMissingBitmapGlyphs, gameplayTest, registerGameplayContract };
102
+ export { type AnyGameTelemetry, GAME_TELEMETRY_GLOBAL, type GameTelemetry, type GameplayContractHandle, GameplayContractMetadata, type GameplayContractOptions, type GameplayTestArguments, type GameplayTestContext, type GameplayTestOptions, HeadlessGameHost, HeadlessGameplayEvidenceRequirements, assertSceneInteractiveHealth, assertSceneRuntimeHealth, assertSceneTextHealth, collectMissingBitmapGlyphs, gameplayTest, getGameTelemetry, installGameTelemetry, registerGameplayContract };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-CusQfLYQ.js';
2
- export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-CusQfLYQ.js';
1
+ import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-CCJo4Qhk.js';
2
+ export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-CCJo4Qhk.js';
3
3
  import * as Phaser from 'phaser';
4
4
  import { TestContext, TestOptions } from 'vitest';
5
5
 
@@ -68,6 +68,29 @@ declare module "vitest" {
68
68
  }
69
69
  }
70
70
 
71
+ /** The single global entry point used by game-owned HEADLESS telemetry. */
72
+ declare const GAME_TELEMETRY_GLOBAL: "gameTelemetry";
73
+ /**
74
+ * A game-owned observation and command boundary for deterministic runtime tests.
75
+ * Snapshot fields and command names intentionally remain specific to each game.
76
+ */
77
+ interface GameTelemetry<SessionSnapshot, Controls extends object, Fixtures extends object = Record<string, never>> {
78
+ version: 1;
79
+ read: {
80
+ session(): SessionSnapshot;
81
+ };
82
+ controls: Controls;
83
+ fixtures?: Fixtures;
84
+ }
85
+ type AnyGameTelemetry = GameTelemetry<unknown, object, object>;
86
+ /**
87
+ * Install a game- or Scene-owned Telemetry handle and remove it on lifecycle exit.
88
+ * Game ownership survives Scene transitions; Scene ownership is useful for local facades.
89
+ */
90
+ declare function installGameTelemetry<Telemetry extends AnyGameTelemetry>(owner: Phaser.Game | Phaser.Scene, telemetry: Telemetry): () => void;
91
+ /** Read the current typed game handle or fail with a direct setup diagnosis. */
92
+ declare function getGameTelemetry<Telemetry extends AnyGameTelemetry>(): Telemetry;
93
+
71
94
  declare function assertSceneRuntimeHealth(scene: Phaser.Scene): void;
72
95
  declare function assertSceneInteractiveHealth(scene: Phaser.Scene): void;
73
96
 
@@ -76,4 +99,4 @@ declare function collectMissingBitmapGlyphs(text: string, chars: Record<number,
76
99
  /** 在文字状态损坏时抛出适合 CI 和编码智能体处理的错误。 */
77
100
  declare function assertSceneTextHealth(scene: Phaser.Scene): void;
78
101
 
79
- export { type GameplayContractHandle, GameplayContractMetadata, type GameplayContractOptions, type GameplayTestArguments, type GameplayTestContext, type GameplayTestOptions, HeadlessGameHost, HeadlessGameplayEvidenceRequirements, assertSceneInteractiveHealth, assertSceneRuntimeHealth, assertSceneTextHealth, collectMissingBitmapGlyphs, gameplayTest, registerGameplayContract };
102
+ export { type AnyGameTelemetry, GAME_TELEMETRY_GLOBAL, type GameTelemetry, type GameplayContractHandle, GameplayContractMetadata, type GameplayContractOptions, type GameplayTestArguments, type GameplayTestContext, type GameplayTestOptions, HeadlessGameHost, HeadlessGameplayEvidenceRequirements, assertSceneInteractiveHealth, assertSceneRuntimeHealth, assertSceneTextHealth, collectMissingBitmapGlyphs, gameplayTest, getGameTelemetry, installGameTelemetry, registerGameplayContract };
package/dist/index.js CHANGED
@@ -30,12 +30,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ GAME_TELEMETRY_GLOBAL: () => GAME_TELEMETRY_GLOBAL,
33
34
  assertSceneInteractiveHealth: () => assertSceneInteractiveHealth,
34
35
  assertSceneRuntimeHealth: () => assertSceneRuntimeHealth,
35
36
  assertSceneTextHealth: () => assertSceneTextHealth,
36
37
  collectMissingBitmapGlyphs: () => collectMissingBitmapGlyphs,
37
38
  createHeadlessGame: () => createHeadlessGame,
38
39
  gameplayTest: () => gameplayTest,
40
+ getGameTelemetry: () => getGameTelemetry,
41
+ installGameTelemetry: () => installGameTelemetry,
39
42
  registerGameplayContract: () => registerGameplayContract
40
43
  });
41
44
  module.exports = __toCommonJS(src_exports);
@@ -1215,13 +1218,62 @@ function gameplayTest(name, options, run) {
1215
1218
  await run({ context, contract });
1216
1219
  });
1217
1220
  }
1221
+
1222
+ // src/game-telemetry.ts
1223
+ var Phaser4 = __toESM(require("phaser"));
1224
+ var GAME_TELEMETRY_GLOBAL = "gameTelemetry";
1225
+ function telemetryGlobal() {
1226
+ return globalThis;
1227
+ }
1228
+ function assertTelemetryShell(value) {
1229
+ if (value.version !== 1 || !value.read || typeof value.read.session !== "function" || !value.controls || typeof value.controls !== "object") {
1230
+ throw new Error(
1231
+ "Game Telemetry must provide version 1, read.session(), and a controls object."
1232
+ );
1233
+ }
1234
+ }
1235
+ function installGameTelemetry(owner, telemetry) {
1236
+ assertTelemetryShell(telemetry);
1237
+ const scope = telemetryGlobal();
1238
+ if (scope[GAME_TELEMETRY_GLOBAL]) {
1239
+ throw new Error(
1240
+ "Only one active owner may install globalThis.gameTelemetry. Use one game-owned facade or clean up the current Scene-owned facade first."
1241
+ );
1242
+ }
1243
+ const events = owner.events;
1244
+ const lifecycleEvents = owner instanceof Phaser4.Game ? [Phaser4.Core.Events.DESTROY] : [Phaser4.Scenes.Events.SHUTDOWN, Phaser4.Scenes.Events.DESTROY];
1245
+ let installed = true;
1246
+ const cleanup = () => {
1247
+ if (!installed) return;
1248
+ installed = false;
1249
+ for (const event of lifecycleEvents) events.off(event, cleanup);
1250
+ if (scope[GAME_TELEMETRY_GLOBAL] === telemetry) {
1251
+ scope[GAME_TELEMETRY_GLOBAL] = void 0;
1252
+ }
1253
+ };
1254
+ scope[GAME_TELEMETRY_GLOBAL] = telemetry;
1255
+ for (const event of lifecycleEvents) events.once(event, cleanup);
1256
+ return cleanup;
1257
+ }
1258
+ function getGameTelemetry() {
1259
+ const telemetry = telemetryGlobal()[GAME_TELEMETRY_GLOBAL];
1260
+ if (!telemetry) {
1261
+ throw new Error(
1262
+ "Game Telemetry is not installed. Install it from a Scene create() method with a Game or Scene lifecycle owner."
1263
+ );
1264
+ }
1265
+ return telemetry;
1266
+ }
1218
1267
  // Annotate the CommonJS export names for ESM import in node:
1219
1268
  0 && (module.exports = {
1269
+ GAME_TELEMETRY_GLOBAL,
1220
1270
  assertSceneInteractiveHealth,
1221
1271
  assertSceneRuntimeHealth,
1222
1272
  assertSceneTextHealth,
1223
1273
  collectMissingBitmapGlyphs,
1224
1274
  createHeadlessGame,
1225
1275
  gameplayTest,
1276
+ getGameTelemetry,
1277
+ installGameTelemetry,
1226
1278
  registerGameplayContract
1227
1279
  });
package/dist/index.mjs CHANGED
@@ -1173,12 +1173,61 @@ function gameplayTest(name, options, run) {
1173
1173
  await run({ context, contract });
1174
1174
  });
1175
1175
  }
1176
+
1177
+ // src/game-telemetry.ts
1178
+ import * as Phaser4 from "phaser";
1179
+ var GAME_TELEMETRY_GLOBAL = "gameTelemetry";
1180
+ function telemetryGlobal() {
1181
+ return globalThis;
1182
+ }
1183
+ function assertTelemetryShell(value) {
1184
+ if (value.version !== 1 || !value.read || typeof value.read.session !== "function" || !value.controls || typeof value.controls !== "object") {
1185
+ throw new Error(
1186
+ "Game Telemetry must provide version 1, read.session(), and a controls object."
1187
+ );
1188
+ }
1189
+ }
1190
+ function installGameTelemetry(owner, telemetry) {
1191
+ assertTelemetryShell(telemetry);
1192
+ const scope = telemetryGlobal();
1193
+ if (scope[GAME_TELEMETRY_GLOBAL]) {
1194
+ throw new Error(
1195
+ "Only one active owner may install globalThis.gameTelemetry. Use one game-owned facade or clean up the current Scene-owned facade first."
1196
+ );
1197
+ }
1198
+ const events = owner.events;
1199
+ const lifecycleEvents = owner instanceof Phaser4.Game ? [Phaser4.Core.Events.DESTROY] : [Phaser4.Scenes.Events.SHUTDOWN, Phaser4.Scenes.Events.DESTROY];
1200
+ let installed = true;
1201
+ const cleanup = () => {
1202
+ if (!installed) return;
1203
+ installed = false;
1204
+ for (const event of lifecycleEvents) events.off(event, cleanup);
1205
+ if (scope[GAME_TELEMETRY_GLOBAL] === telemetry) {
1206
+ scope[GAME_TELEMETRY_GLOBAL] = void 0;
1207
+ }
1208
+ };
1209
+ scope[GAME_TELEMETRY_GLOBAL] = telemetry;
1210
+ for (const event of lifecycleEvents) events.once(event, cleanup);
1211
+ return cleanup;
1212
+ }
1213
+ function getGameTelemetry() {
1214
+ const telemetry = telemetryGlobal()[GAME_TELEMETRY_GLOBAL];
1215
+ if (!telemetry) {
1216
+ throw new Error(
1217
+ "Game Telemetry is not installed. Install it from a Scene create() method with a Game or Scene lifecycle owner."
1218
+ );
1219
+ }
1220
+ return telemetry;
1221
+ }
1176
1222
  export {
1223
+ GAME_TELEMETRY_GLOBAL,
1177
1224
  assertSceneInteractiveHealth,
1178
1225
  assertSceneRuntimeHealth,
1179
1226
  assertSceneTextHealth,
1180
1227
  collectMissingBitmapGlyphs,
1181
1228
  createHeadlessGame,
1182
1229
  gameplayTest,
1230
+ getGameTelemetry,
1231
+ installGameTelemetry,
1183
1232
  registerGameplayContract
1184
1233
  };