miaoda-game-devkit 0.2.5 → 0.2.7

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
@@ -88,8 +88,9 @@ DOM 到 Phaser 的输入、命中测试和游戏状态变化,不验证 Canvas/
88
88
  `host.checkpoint("recovery")` 等稳定检查点,并在清单中通过 `requireCheckpoint` 声明。
89
89
  运行结束会输出稳定的 `GAMEPLAY_AUDIT: ALL CONTRACTS PASSED` 或
90
90
  `GAMEPLAY_AUDIT: ... FAILED` 标识。该标识只表示玩法证据审计;`pnpm test` 的整体结果
91
- 必须以最后的 `TEST_RESULT: PASS` 或 `TEST_RESULT: FAIL` 为准。失败报告会合并重复的问题,
92
- 并给出按类别去重的修复建议,先处理列出的原始测试错误,再重新运行 `pnpm test`。
91
+ 必须以最后的 `TEST_RESULT: PASS` 或 `TEST_RESULT: FAIL` 为准。失败报告按根因合并问题并
92
+ 给出直接修复步骤;测试文件无法加载或配置要求的测试尚未建立时,不再继续报告 Scene
93
+ 注册等派生问题。先处理第一类错误,再重新运行 `pnpm test`。
93
94
 
94
95
  共享 Vitest 配置使用 `minimal` reporter 和 `text-summary` coverage reporter,保留失败
95
96
  定位、Scene 覆盖摘要和最终机器标识,同时避免输出完整测试列表和逐文件噪声。
@@ -18,7 +18,9 @@ interface HeadlessGameHost<TScene extends Phaser.Scene> {
18
18
  /** 推进完整 Phaser 帧并等待异步 update 或生命周期完成。 */
19
19
  stepFramesAsync(count?: number): Promise<void>;
20
20
  /** 在限定帧数内推进游戏,直到条件成立。 */
21
- stepUntil(condition: () => boolean, maxFrames?: number): number;
21
+ stepUntil(condition: () => boolean, maxFrames?: number | {
22
+ maxFrames?: number;
23
+ }): number;
22
24
  /** 精确推进指定次数的 Arcade Physics 单步。 */
23
25
  stepPhysics(steps?: number): void;
24
26
  /** 记录由测试定义的可序列化玩法检查点。 */
@@ -18,7 +18,9 @@ interface HeadlessGameHost<TScene extends Phaser.Scene> {
18
18
  /** 推进完整 Phaser 帧并等待异步 update 或生命周期完成。 */
19
19
  stepFramesAsync(count?: number): Promise<void>;
20
20
  /** 在限定帧数内推进游戏,直到条件成立。 */
21
- stepUntil(condition: () => boolean, maxFrames?: number): number;
21
+ stepUntil(condition: () => boolean, maxFrames?: number | {
22
+ maxFrames?: number;
23
+ }): number;
22
24
  /** 精确推进指定次数的 Arcade Physics 单步。 */
23
25
  stepPhysics(steps?: number): void;
24
26
  /** 记录由测试定义的可序列化玩法检查点。 */
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-XBGq_jIV.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-XBGq_jIV.mjs';
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';
3
3
  import * as Phaser from 'phaser';
4
4
  import { TestContext, TestOptions } from 'vitest';
5
5
 
@@ -23,6 +23,8 @@ interface GameplayTestOptions extends GameplayContractOptions {
23
23
  interface GameplayContractHandle<TScene extends Phaser.Scene> {
24
24
  /** 将当前 contract 绑定到实际创建的 HEADLESS host。 */
25
25
  attachHost(host: HeadlessGameHost<TScene>): void;
26
+ /** 记录一个由测试先断言、再声明的玩法检查点。 */
27
+ checkpoint(id: string): void;
26
28
  /** 立即验证证据;通常由 onTestFinished 自动执行。 */
27
29
  verify(): void;
28
30
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-XBGq_jIV.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-XBGq_jIV.js';
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';
3
3
  import * as Phaser from 'phaser';
4
4
  import { TestContext, TestOptions } from 'vitest';
5
5
 
@@ -23,6 +23,8 @@ interface GameplayTestOptions extends GameplayContractOptions {
23
23
  interface GameplayContractHandle<TScene extends Phaser.Scene> {
24
24
  /** 将当前 contract 绑定到实际创建的 HEADLESS host。 */
25
25
  attachHost(host: HeadlessGameHost<TScene>): void;
26
+ /** 记录一个由测试先断言、再声明的玩法检查点。 */
27
+ checkpoint(id: string): void;
26
28
  /** 立即验证证据;通常由 onTestFinished 自动执行。 */
27
29
  verify(): void;
28
30
  }
package/dist/index.js CHANGED
@@ -1021,7 +1021,8 @@ async function createHeadlessGame(scene, options = {}) {
1021
1021
  stepFrames(count);
1022
1022
  await settleRuntime();
1023
1023
  },
1024
- stepUntil(condition, maxFrames = 120) {
1024
+ stepUntil(condition, maxFramesOrOptions = 120) {
1025
+ const maxFrames = typeof maxFramesOrOptions === "number" ? maxFramesOrOptions : maxFramesOrOptions.maxFrames ?? 120;
1025
1026
  assertStepCount(maxFrames, "stepUntil maxFrames");
1026
1027
  throwRuntimeError();
1027
1028
  for (let frame = 0; frame <= maxFrames; frame += 1) {
@@ -1193,6 +1194,14 @@ function registerGameplayContract(context, options) {
1193
1194
  }
1194
1195
  host = currentHost;
1195
1196
  },
1197
+ checkpoint(id) {
1198
+ if (!host) {
1199
+ throw new Error(
1200
+ `Gameplay contract "${options.id}" must attach a HEADLESS host before recording a checkpoint.`
1201
+ );
1202
+ }
1203
+ host.checkpoint(id);
1204
+ },
1196
1205
  verify
1197
1206
  };
1198
1207
  }
package/dist/index.mjs CHANGED
@@ -979,7 +979,8 @@ async function createHeadlessGame(scene, options = {}) {
979
979
  stepFrames(count);
980
980
  await settleRuntime();
981
981
  },
982
- stepUntil(condition, maxFrames = 120) {
982
+ stepUntil(condition, maxFramesOrOptions = 120) {
983
+ const maxFrames = typeof maxFramesOrOptions === "number" ? maxFramesOrOptions : maxFramesOrOptions.maxFrames ?? 120;
983
984
  assertStepCount(maxFrames, "stepUntil maxFrames");
984
985
  throwRuntimeError();
985
986
  for (let frame = 0; frame <= maxFrames; frame += 1) {
@@ -1151,6 +1152,14 @@ function registerGameplayContract(context, options) {
1151
1152
  }
1152
1153
  host = currentHost;
1153
1154
  },
1155
+ checkpoint(id) {
1156
+ if (!host) {
1157
+ throw new Error(
1158
+ `Gameplay contract "${options.id}" must attach a HEADLESS host before recording a checkpoint.`
1159
+ );
1160
+ }
1161
+ host.checkpoint(id);
1162
+ },
1154
1163
  verify
1155
1164
  };
1156
1165
  }
@@ -334,18 +334,21 @@ function auditGameplayRun(options2, tests) {
334
334
  observedEvidence.flatMap((evidence) => evidence.registeredScenes)
335
335
  );
336
336
  const expectedScenes = normalizeSet(options2.scenes);
337
- for (const scene of expectedScenes) {
338
- if (!registeredScenes.includes(scene)) {
339
- issues.push(
340
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
341
- );
337
+ if (observedEvidence.length > 0) {
338
+ for (const scene of expectedScenes) {
339
+ if (!registeredScenes.includes(scene)) {
340
+ issues.push(
341
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
342
+ );
343
+ }
344
+ }
345
+ for (const scene of registeredScenes) {
346
+ if (!expectedScenes.includes(scene)) {
347
+ issues.push(
348
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
349
+ );
350
+ }
342
351
  }
343
- }
344
- for (const scene of registeredScenes) {
345
- if (!expectedScenes.includes(scene))
346
- issues.push(
347
- `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
348
- );
349
352
  }
350
353
  return { passed: issues.length === 0, issues: [...new Set(issues)] };
351
354
  }
@@ -356,18 +359,31 @@ function formatGameplayIssues(issues) {
356
359
  const groups = /* @__PURE__ */ new Map();
357
360
  const repairKeys = /* @__PURE__ */ new Set();
358
361
  const repairs = [];
362
+ const missingByFile = /* @__PURE__ */ new Map();
363
+ const missingIds = /* @__PURE__ */ new Set();
364
+ for (const issue of issues) {
365
+ const match = issue.match(
366
+ /没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
367
+ );
368
+ if (!match) continue;
369
+ const [, id, file] = match;
370
+ missingIds.add(id);
371
+ const ids = missingByFile.get(file) ?? [];
372
+ if (!ids.includes(id)) ids.push(id);
373
+ missingByFile.set(file, ids);
374
+ }
359
375
  const repairText = {
360
376
  input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
361
377
  frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
362
378
  physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
363
- transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
379
+ transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
364
380
  visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
365
381
  restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
366
- checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
382
+ checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
367
383
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
368
- registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
369
- missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
370
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
384
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
385
+ missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
386
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
371
387
  };
372
388
  const getCategory = (issue) => {
373
389
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -395,9 +411,26 @@ function formatGameplayIssues(issues) {
395
411
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
396
412
  return { category: "other", key: `other:${issue}` };
397
413
  };
414
+ for (const [file, ids] of missingByFile) {
415
+ groups.set(`missing:${file}`, {
416
+ text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
417
+ ids: [],
418
+ key: `missing:${file}`
419
+ });
420
+ }
421
+ if (missingByFile.size > 0) {
422
+ repairKeys.add("missing");
423
+ repairs.push(repairText.missing);
424
+ }
398
425
  for (const rawIssue of [...new Set(issues)]) {
399
426
  const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
400
- const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
427
+ if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
428
+ const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
429
+ if (missingContract && missingIds.has(missingContract)) continue;
430
+ if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
431
+ continue;
432
+ }
433
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
401
434
  const id = compact.match(/contract "([^"]+)"/)?.[1];
402
435
  const { category, key } = getCategory(compact);
403
436
  const group = groups.get(key);
@@ -414,7 +447,7 @@ function formatGameplayIssues(issues) {
414
447
  const grouped = [...groups.values()];
415
448
  const visible = grouped.slice(0, 8).map((group) => {
416
449
  if (group.ids.length <= 1) return group.text;
417
- return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
450
+ return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
418
451
  });
419
452
  return {
420
453
  issues: visible.map(
@@ -758,7 +791,7 @@ describe("gameplay audit", () => {
758
791
  expect(result.issues).toHaveLength(4);
759
792
  expect(result.repairs).toHaveLength(3);
760
793
  expect(result.repairs[0]).toContain("\u8F93\u5165");
761
- expect(result.repairs[1]).toContain("\u8F6C\u573A");
794
+ expect(result.repairs[1]).toContain("\u573A\u666F\u5207\u6362");
762
795
  });
763
796
  it("\u5C06\u591A\u68C0\u67E5\u9879\u7684\u7F3A\u53E3\u9650\u5236\u4E3A\u95EE\u9898\u7EC4\u800C\u4E0D\u662F\u539F\u59CB\u65E5\u5FD7\u884C", () => {
764
797
  const issues = Array.from(
@@ -770,4 +803,18 @@ describe("gameplay audit", () => {
770
803
  expect(result.repairs).toHaveLength(1);
771
804
  expect(result.omitted).toBe(0);
772
805
  });
806
+ it("\u5C06\u7F3A\u5931\u6D4B\u8BD5\u5408\u5E76\u4E3A\u4E00\u4E2A\u53EF\u6267\u884C\u4FEE\u590D\u5E76\u9690\u85CF Scene \u7EA7\u8054\u9519\u8BEF", () => {
807
+ const result = formatGameplayIssues([
808
+ '\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "IndexScene.boot" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08tests/production-game.test.ts\uFF09\u3002',
809
+ '\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "BattleScene.playthrough" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08tests/production-game.test.ts\uFF09\u3002',
810
+ '\u7F3A\u5C11 gameplay contract "IndexScene.boot"\u3002',
811
+ '\u7F3A\u5C11 gameplay contract "BattleScene.playthrough"\u3002',
812
+ '\u751F\u4EA7 Scene "BattleScene" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002'
813
+ ]);
814
+ expect(result.issues).toEqual([
815
+ "tests/production-game.test.ts \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1AIndexScene.boot\u3001BattleScene.playthrough\u3002"
816
+ ]);
817
+ expect(result.repairs).toHaveLength(1);
818
+ expect(result.repairs[0]).toContain("vitest.config.ts");
819
+ });
773
820
  });
@@ -983,7 +983,8 @@ async function createHeadlessGame(scene, options = {}) {
983
983
  stepFrames(count);
984
984
  await settleRuntime();
985
985
  },
986
- stepUntil(condition, maxFrames = 120) {
986
+ stepUntil(condition, maxFramesOrOptions = 120) {
987
+ const maxFrames = typeof maxFramesOrOptions === "number" ? maxFramesOrOptions : maxFramesOrOptions.maxFrames ?? 120;
987
988
  assertStepCount(maxFrames, "stepUntil maxFrames");
988
989
  throwRuntimeError();
989
990
  for (let frame = 0; frame <= maxFrames; frame += 1) {
@@ -1155,6 +1156,14 @@ function registerGameplayContract(context, options) {
1155
1156
  }
1156
1157
  host = currentHost;
1157
1158
  },
1159
+ checkpoint(id) {
1160
+ if (!host) {
1161
+ throw new Error(
1162
+ `Gameplay contract "${options.id}" must attach a HEADLESS host before recording a checkpoint.`
1163
+ );
1164
+ }
1165
+ host.checkpoint(id);
1166
+ },
1158
1167
  verify
1159
1168
  };
1160
1169
  }
@@ -1190,7 +1199,8 @@ describe("gameplay contract lifecycle integration", () => {
1190
1199
  id: "InputScene.hit",
1191
1200
  kind: "playthrough",
1192
1201
  requireInput: true,
1193
- requireFrameAdvance: true
1202
+ requireFrameAdvance: true,
1203
+ requireCheckpoint: "hit-recorded"
1194
1204
  });
1195
1205
  let host;
1196
1206
  try {
@@ -1199,12 +1209,14 @@ describe("gameplay contract lifecycle integration", () => {
1199
1209
  host.stepFrames();
1200
1210
  await host.input.mouse.click(40, 40);
1201
1211
  expect(host.scene.hits).toBe(1);
1212
+ contract.checkpoint("hit-recorded");
1202
1213
  host.destroy();
1203
1214
  await callbacks[0]?.();
1204
1215
  expect(context.task.meta.gameplayContract).toMatchObject({
1205
1216
  id: "InputScene.hit",
1206
1217
  evidence: {
1207
1218
  clicks: 1,
1219
+ checkpoints: ["hit-recorded"],
1208
1220
  frames: 1,
1209
1221
  mouseEvents: 3
1210
1222
  }
@@ -983,7 +983,8 @@ async function createHeadlessGame(scene, options = {}) {
983
983
  stepFrames(count);
984
984
  await settleRuntime();
985
985
  },
986
- stepUntil(condition, maxFrames = 120) {
986
+ stepUntil(condition, maxFramesOrOptions = 120) {
987
+ const maxFrames = typeof maxFramesOrOptions === "number" ? maxFramesOrOptions : maxFramesOrOptions.maxFrames ?? 120;
987
988
  assertStepCount(maxFrames, "stepUntil maxFrames");
988
989
  throwRuntimeError();
989
990
  for (let frame = 0; frame <= maxFrames; frame += 1) {
@@ -1149,6 +1150,9 @@ describe("Phaser HEADLESS host", () => {
1149
1150
  expect(() => boundedHost.stepUntil(() => false, 1.5)).toThrow(
1150
1151
  "stepUntil maxFrames must be a non-negative safe integer"
1151
1152
  );
1153
+ expect(() => boundedHost.stepUntil(() => false, { maxFrames: 0 })).toThrow(
1154
+ "Condition was not met within 0 complete Phaser frames"
1155
+ );
1152
1156
  expect(() => boundedHost.stepPhysics(-1)).toThrow(
1153
1157
  "stepPhysics steps must be a non-negative safe integer"
1154
1158
  );
@@ -339,18 +339,21 @@ function auditGameplayRun(options, tests) {
339
339
  observedEvidence.flatMap((evidence) => evidence.registeredScenes)
340
340
  );
341
341
  const expectedScenes = normalizeSet(options.scenes);
342
- for (const scene of expectedScenes) {
343
- if (!registeredScenes.includes(scene)) {
344
- issues.push(
345
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
346
- );
342
+ if (observedEvidence.length > 0) {
343
+ for (const scene of expectedScenes) {
344
+ if (!registeredScenes.includes(scene)) {
345
+ issues.push(
346
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
347
+ );
348
+ }
349
+ }
350
+ for (const scene of registeredScenes) {
351
+ if (!expectedScenes.includes(scene)) {
352
+ issues.push(
353
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
354
+ );
355
+ }
347
356
  }
348
- }
349
- for (const scene of registeredScenes) {
350
- if (!expectedScenes.includes(scene))
351
- issues.push(
352
- `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
353
- );
354
357
  }
355
358
  return { passed: issues.length === 0, issues: [...new Set(issues)] };
356
359
  }
@@ -378,18 +381,31 @@ function formatGameplayIssues(issues) {
378
381
  const groups = /* @__PURE__ */ new Map();
379
382
  const repairKeys = /* @__PURE__ */ new Set();
380
383
  const repairs = [];
384
+ const missingByFile = /* @__PURE__ */ new Map();
385
+ const missingIds = /* @__PURE__ */ new Set();
386
+ for (const issue of issues) {
387
+ const match = issue.match(
388
+ /没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
389
+ );
390
+ if (!match) continue;
391
+ const [, id, file] = match;
392
+ missingIds.add(id);
393
+ const ids = missingByFile.get(file) ?? [];
394
+ if (!ids.includes(id)) ids.push(id);
395
+ missingByFile.set(file, ids);
396
+ }
381
397
  const repairText = {
382
398
  input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
383
399
  frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
384
400
  physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
385
- transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
401
+ transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
386
402
  visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
387
403
  restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
388
- checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
404
+ checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
389
405
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
390
- registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
391
- missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
392
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
406
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
407
+ missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
408
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
393
409
  };
394
410
  const getCategory = (issue) => {
395
411
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -417,9 +433,26 @@ function formatGameplayIssues(issues) {
417
433
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
418
434
  return { category: "other", key: `other:${issue}` };
419
435
  };
436
+ for (const [file, ids] of missingByFile) {
437
+ groups.set(`missing:${file}`, {
438
+ text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
439
+ ids: [],
440
+ key: `missing:${file}`
441
+ });
442
+ }
443
+ if (missingByFile.size > 0) {
444
+ repairKeys.add("missing");
445
+ repairs.push(repairText.missing);
446
+ }
420
447
  for (const rawIssue of [...new Set(issues)]) {
421
448
  const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
422
- const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
449
+ if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
450
+ const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
451
+ if (missingContract && missingIds.has(missingContract)) continue;
452
+ if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
453
+ continue;
454
+ }
455
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
423
456
  const id = compact.match(/contract "([^"]+)"/)?.[1];
424
457
  const { category, key } = getCategory(compact);
425
458
  const group = groups.get(key);
@@ -436,7 +469,7 @@ function formatGameplayIssues(issues) {
436
469
  const grouped = [...groups.values()];
437
470
  const visible = grouped.slice(0, 8).map((group) => {
438
471
  if (group.ids.length <= 1) return group.text;
439
- return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
472
+ return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
440
473
  });
441
474
  return {
442
475
  issues: visible.map(
@@ -450,9 +483,7 @@ function printIssues(stage, issues) {
450
483
  if (issues.length === 0) return;
451
484
  console.error(`
452
485
  GAMEPLAY_AUDIT: ${stage} FAILED`);
453
- console.error(
454
- "\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
455
- );
486
+ console.error("\u73A9\u6CD5\u6D4B\u8BD5\u672A\u901A\u8FC7\u3002\u8BF7\u5148\u5904\u7406\u7B2C\u4E00\u7C7B\u95EE\u9898\uFF0C\u91CD\u65B0\u8FD0\u884C\u540E\u518D\u5904\u7406\u540E\u7EED\u95EE\u9898\u3002");
456
487
  const formatted = formatGameplayIssues(issues);
457
488
  for (const issue of formatted.issues) console.error(`- ${issue}`);
458
489
  if (formatted.repairs.length > 0) {
@@ -519,9 +550,6 @@ var GameplayAuditReporter = class {
519
550
  ...result.issues
520
551
  ];
521
552
  if (combinedIssues.length > 0) {
522
- if (this.preflightIssues.size > 0) {
523
- console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
524
- }
525
553
  printIssues("FINAL", [...new Set(combinedIssues)]);
526
554
  process.exitCode = 1;
527
555
  } else {
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-XBGq_jIV.mjs';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-CusQfLYQ.mjs';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-XBGq_jIV.js';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-CusQfLYQ.js';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
@@ -359,18 +359,21 @@ function auditGameplayRun(options, tests) {
359
359
  observedEvidence.flatMap((evidence) => evidence.registeredScenes)
360
360
  );
361
361
  const expectedScenes = normalizeSet(options.scenes);
362
- for (const scene of expectedScenes) {
363
- if (!registeredScenes.includes(scene)) {
364
- issues.push(
365
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
366
- );
362
+ if (observedEvidence.length > 0) {
363
+ for (const scene of expectedScenes) {
364
+ if (!registeredScenes.includes(scene)) {
365
+ issues.push(
366
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
367
+ );
368
+ }
369
+ }
370
+ for (const scene of registeredScenes) {
371
+ if (!expectedScenes.includes(scene)) {
372
+ issues.push(
373
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
374
+ );
375
+ }
367
376
  }
368
- }
369
- for (const scene of registeredScenes) {
370
- if (!expectedScenes.includes(scene))
371
- issues.push(
372
- `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
373
- );
374
377
  }
375
378
  return { passed: issues.length === 0, issues: [...new Set(issues)] };
376
379
  }
@@ -398,18 +401,31 @@ function formatGameplayIssues(issues) {
398
401
  const groups = /* @__PURE__ */ new Map();
399
402
  const repairKeys = /* @__PURE__ */ new Set();
400
403
  const repairs = [];
404
+ const missingByFile = /* @__PURE__ */ new Map();
405
+ const missingIds = /* @__PURE__ */ new Set();
406
+ for (const issue of issues) {
407
+ const match = issue.match(
408
+ /没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
409
+ );
410
+ if (!match) continue;
411
+ const [, id, file] = match;
412
+ missingIds.add(id);
413
+ const ids = missingByFile.get(file) ?? [];
414
+ if (!ids.includes(id)) ids.push(id);
415
+ missingByFile.set(file, ids);
416
+ }
401
417
  const repairText = {
402
418
  input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
403
419
  frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
404
420
  physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
405
- transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
421
+ transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
406
422
  visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
407
423
  restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
408
- checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
424
+ checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
409
425
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
410
- registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
411
- missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
412
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
426
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
427
+ missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
428
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
413
429
  };
414
430
  const getCategory = (issue) => {
415
431
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -437,9 +453,26 @@ function formatGameplayIssues(issues) {
437
453
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
438
454
  return { category: "other", key: `other:${issue}` };
439
455
  };
456
+ for (const [file, ids] of missingByFile) {
457
+ groups.set(`missing:${file}`, {
458
+ text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
459
+ ids: [],
460
+ key: `missing:${file}`
461
+ });
462
+ }
463
+ if (missingByFile.size > 0) {
464
+ repairKeys.add("missing");
465
+ repairs.push(repairText.missing);
466
+ }
440
467
  for (const rawIssue of [...new Set(issues)]) {
441
468
  const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
442
- const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
469
+ if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
470
+ const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
471
+ if (missingContract && missingIds.has(missingContract)) continue;
472
+ if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
473
+ continue;
474
+ }
475
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
443
476
  const id = compact.match(/contract "([^"]+)"/)?.[1];
444
477
  const { category, key } = getCategory(compact);
445
478
  const group = groups.get(key);
@@ -456,7 +489,7 @@ function formatGameplayIssues(issues) {
456
489
  const grouped = [...groups.values()];
457
490
  const visible = grouped.slice(0, 8).map((group) => {
458
491
  if (group.ids.length <= 1) return group.text;
459
- return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
492
+ return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
460
493
  });
461
494
  return {
462
495
  issues: visible.map(
@@ -470,9 +503,7 @@ function printIssues(stage, issues) {
470
503
  if (issues.length === 0) return;
471
504
  console.error(`
472
505
  GAMEPLAY_AUDIT: ${stage} FAILED`);
473
- console.error(
474
- "\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
475
- );
506
+ console.error("\u73A9\u6CD5\u6D4B\u8BD5\u672A\u901A\u8FC7\u3002\u8BF7\u5148\u5904\u7406\u7B2C\u4E00\u7C7B\u95EE\u9898\uFF0C\u91CD\u65B0\u8FD0\u884C\u540E\u518D\u5904\u7406\u540E\u7EED\u95EE\u9898\u3002");
476
507
  const formatted = formatGameplayIssues(issues);
477
508
  for (const issue of formatted.issues) console.error(`- ${issue}`);
478
509
  if (formatted.repairs.length > 0) {
@@ -539,9 +570,6 @@ var GameplayAuditReporter = class {
539
570
  ...result.issues
540
571
  ];
541
572
  if (combinedIssues.length > 0) {
542
- if (this.preflightIssues.size > 0) {
543
- console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
544
- }
545
573
  printIssues("FINAL", [...new Set(combinedIssues)]);
546
574
  process.exitCode = 1;
547
575
  } else {
@@ -335,18 +335,21 @@ function auditGameplayRun(options, tests) {
335
335
  observedEvidence.flatMap((evidence) => evidence.registeredScenes)
336
336
  );
337
337
  const expectedScenes = normalizeSet(options.scenes);
338
- for (const scene of expectedScenes) {
339
- if (!registeredScenes.includes(scene)) {
340
- issues.push(
341
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
342
- );
338
+ if (observedEvidence.length > 0) {
339
+ for (const scene of expectedScenes) {
340
+ if (!registeredScenes.includes(scene)) {
341
+ issues.push(
342
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
343
+ );
344
+ }
345
+ }
346
+ for (const scene of registeredScenes) {
347
+ if (!expectedScenes.includes(scene)) {
348
+ issues.push(
349
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
350
+ );
351
+ }
343
352
  }
344
- }
345
- for (const scene of registeredScenes) {
346
- if (!expectedScenes.includes(scene))
347
- issues.push(
348
- `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
349
- );
350
353
  }
351
354
  return { passed: issues.length === 0, issues: [...new Set(issues)] };
352
355
  }
@@ -374,18 +377,31 @@ function formatGameplayIssues(issues) {
374
377
  const groups = /* @__PURE__ */ new Map();
375
378
  const repairKeys = /* @__PURE__ */ new Set();
376
379
  const repairs = [];
380
+ const missingByFile = /* @__PURE__ */ new Map();
381
+ const missingIds = /* @__PURE__ */ new Set();
382
+ for (const issue of issues) {
383
+ const match = issue.match(
384
+ /没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
385
+ );
386
+ if (!match) continue;
387
+ const [, id, file] = match;
388
+ missingIds.add(id);
389
+ const ids = missingByFile.get(file) ?? [];
390
+ if (!ids.includes(id)) ids.push(id);
391
+ missingByFile.set(file, ids);
392
+ }
377
393
  const repairText = {
378
394
  input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
379
395
  frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
380
396
  physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
381
- transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
397
+ transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
382
398
  visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
383
399
  restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
384
- checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
400
+ checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
385
401
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
386
- registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
387
- missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
388
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
402
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
403
+ missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
404
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
389
405
  };
390
406
  const getCategory = (issue) => {
391
407
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -413,9 +429,26 @@ function formatGameplayIssues(issues) {
413
429
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
414
430
  return { category: "other", key: `other:${issue}` };
415
431
  };
432
+ for (const [file, ids] of missingByFile) {
433
+ groups.set(`missing:${file}`, {
434
+ text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
435
+ ids: [],
436
+ key: `missing:${file}`
437
+ });
438
+ }
439
+ if (missingByFile.size > 0) {
440
+ repairKeys.add("missing");
441
+ repairs.push(repairText.missing);
442
+ }
416
443
  for (const rawIssue of [...new Set(issues)]) {
417
444
  const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
418
- const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
445
+ if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
446
+ const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
447
+ if (missingContract && missingIds.has(missingContract)) continue;
448
+ if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
449
+ continue;
450
+ }
451
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
419
452
  const id = compact.match(/contract "([^"]+)"/)?.[1];
420
453
  const { category, key } = getCategory(compact);
421
454
  const group = groups.get(key);
@@ -432,7 +465,7 @@ function formatGameplayIssues(issues) {
432
465
  const grouped = [...groups.values()];
433
466
  const visible = grouped.slice(0, 8).map((group) => {
434
467
  if (group.ids.length <= 1) return group.text;
435
- return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
468
+ return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
436
469
  });
437
470
  return {
438
471
  issues: visible.map(
@@ -446,9 +479,7 @@ function printIssues(stage, issues) {
446
479
  if (issues.length === 0) return;
447
480
  console.error(`
448
481
  GAMEPLAY_AUDIT: ${stage} FAILED`);
449
- console.error(
450
- "\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
451
- );
482
+ console.error("\u73A9\u6CD5\u6D4B\u8BD5\u672A\u901A\u8FC7\u3002\u8BF7\u5148\u5904\u7406\u7B2C\u4E00\u7C7B\u95EE\u9898\uFF0C\u91CD\u65B0\u8FD0\u884C\u540E\u518D\u5904\u7406\u540E\u7EED\u95EE\u9898\u3002");
452
483
  const formatted = formatGameplayIssues(issues);
453
484
  for (const issue of formatted.issues) console.error(`- ${issue}`);
454
485
  if (formatted.repairs.length > 0) {
@@ -515,9 +546,6 @@ var GameplayAuditReporter = class {
515
546
  ...result.issues
516
547
  ];
517
548
  if (combinedIssues.length > 0) {
518
- if (this.preflightIssues.size > 0) {
519
- console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
520
- }
521
549
  printIssues("FINAL", [...new Set(combinedIssues)]);
522
550
  process.exitCode = 1;
523
551
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",