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 +3 -2
- package/dist/{gameplay-audit-XBGq_jIV.d.mts → gameplay-audit-CusQfLYQ.d.mts} +3 -1
- package/dist/{gameplay-audit-XBGq_jIV.d.ts → gameplay-audit-CusQfLYQ.d.ts} +3 -1
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +10 -1
- package/dist/index.mjs +10 -1
- package/dist/lint/gameplay-audit.contract.mjs +66 -19
- package/dist/lint/gameplay-contract.contract.mjs +14 -2
- package/dist/lint/phaser-headless.contract.mjs +5 -1
- package/dist/lint/vitest-config.contract.mjs +52 -24
- package/dist/vitest-config.d.mts +1 -1
- package/dist/vitest-config.d.ts +1 -1
- package/dist/vitest-config.js +52 -24
- package/dist/vitest-config.mjs +52 -24
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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: "\
|
|
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: "\
|
|
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
|
|
369
|
-
missing: "\
|
|
370
|
-
test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\
|
|
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
|
-
|
|
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\
|
|
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("\
|
|
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,
|
|
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,
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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: "\
|
|
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: "\
|
|
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
|
|
391
|
-
missing: "\
|
|
392
|
-
test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\
|
|
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
|
-
|
|
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\
|
|
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 {
|
package/dist/vitest-config.d.mts
CHANGED
package/dist/vitest-config.d.ts
CHANGED
package/dist/vitest-config.js
CHANGED
|
@@ -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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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: "\
|
|
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: "\
|
|
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
|
|
411
|
-
missing: "\
|
|
412
|
-
test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\
|
|
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
|
-
|
|
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\
|
|
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 {
|
package/dist/vitest-config.mjs
CHANGED
|
@@ -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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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: "\
|
|
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: "\
|
|
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
|
|
387
|
-
missing: "\
|
|
388
|
-
test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\
|
|
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
|
-
|
|
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\
|
|
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 {
|