miaoda-game-devkit 0.2.3 → 0.2.4
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 +14 -5
- package/dist/lint/gameplay-audit.contract.mjs +134 -11
- package/dist/lint/gameplay-contract.contract.mjs +2 -0
- package/dist/lint/vitest-config.contract.mjs +124 -19
- package/dist/vitest-config.js +122 -17
- package/dist/vitest-config.mjs +122 -17
- package/package.json +1 -28
package/README.md
CHANGED
|
@@ -17,10 +17,14 @@ Scene 命令、restart、业务 checkpoint 和销毁。玩法测试可以调用
|
|
|
17
17
|
`host.assertGameplayEvidence()` 把这些客观事实设为门禁;它不推断业务语义,
|
|
18
18
|
authoritative state 仍必须由测试显式断言。
|
|
19
19
|
|
|
20
|
-
玩法测试应优先使用 `gameplayTest`。它在 Vitest
|
|
21
|
-
在执行阶段绑定 HEADLESS host,并在 `onTestFinished` 校验 Ledger
|
|
22
|
-
|
|
23
|
-
Scene
|
|
20
|
+
玩法测试应优先使用 `gameplayTest`。它在 Vitest 收集阶段写入测试声明,
|
|
21
|
+
在执行阶段绑定 HEADLESS host,并在 `onTestFinished` 校验 Ledger。配置加载阶段会先校验
|
|
22
|
+
交互项目的最低基线:至少一个 `kind: "playthrough"` contract 必须要求真实输入、完整帧和
|
|
23
|
+
语义 checkpoint,每个非入口 Scene 必须有生产转场覆盖;检测到 `scene.restart()` 时还必须
|
|
24
|
+
声明 `flows.restart: true` 和 `kind: "recovery"` contract。配置预检失败时测试和 coverage
|
|
25
|
+
都不会运行。配置通过后,`gameplayAudit` reporter 才会检查测试声明,并在结束时汇总
|
|
26
|
+
缺失、失败、证据不足、Scene 未访问和 registry 不一致。`registerGameplayContract` 保留为
|
|
27
|
+
不参加执行前预检的低层接口。
|
|
24
28
|
|
|
25
29
|
模板直接调用开发工具包提供的命令行程序:
|
|
26
30
|
|
|
@@ -83,7 +87,12 @@ DOM 到 Phaser 的输入、命中测试和游戏状态变化,不验证 Canvas/
|
|
|
83
87
|
断言 authoritative state 后调用 `host.checkpoint("progress")`、
|
|
84
88
|
`host.checkpoint("recovery")` 等稳定检查点,并在清单中通过 `requireCheckpoint` 声明。
|
|
85
89
|
运行结束会输出稳定的 `GAMEPLAY_AUDIT: ALL CONTRACTS PASSED` 或
|
|
86
|
-
`GAMEPLAY_AUDIT: ... FAILED`
|
|
90
|
+
`GAMEPLAY_AUDIT: ... FAILED` 标识。该标识只表示玩法证据审计;`pnpm test` 的整体结果
|
|
91
|
+
必须以最后的 `TEST_RESULT: PASS` 或 `TEST_RESULT: FAIL` 为准。失败报告会合并重复的问题,
|
|
92
|
+
并给出按类别去重的修复建议,先处理列出的原始测试错误,再重新运行 `pnpm test`。
|
|
93
|
+
|
|
94
|
+
共享 Vitest 配置使用 `minimal` reporter 和 `text-summary` coverage reporter,保留失败
|
|
95
|
+
定位、Scene 覆盖摘要和最终机器标识,同时避免输出完整测试列表和逐文件噪声。
|
|
87
96
|
|
|
88
97
|
## 发布顺序
|
|
89
98
|
|
|
@@ -248,12 +248,15 @@ function auditGameplayCollection(options2, file, tests) {
|
|
|
248
248
|
function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
249
249
|
const issues = [];
|
|
250
250
|
const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
|
|
251
|
-
if (requirements.requireInput && inputEvents === 0)
|
|
251
|
+
if (requirements.requireInput && inputEvents === 0) {
|
|
252
252
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
|
|
253
|
-
|
|
253
|
+
}
|
|
254
|
+
if (requirements.requireFrameAdvance && evidence.frames === 0) {
|
|
254
255
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
|
|
255
|
-
|
|
256
|
+
}
|
|
257
|
+
if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
|
|
256
258
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
|
|
259
|
+
}
|
|
257
260
|
for (const target of normalizeList(requirements.requireTransition)) {
|
|
258
261
|
if (!evidence.transitions.some((transition) => transition.to === target)) {
|
|
259
262
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
|
|
@@ -264,21 +267,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
|
264
267
|
issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
|
|
265
268
|
}
|
|
266
269
|
for (const scene of normalizeList(requirements.requireRestart)) {
|
|
267
|
-
if (!evidence.restartedScenes.includes(scene))
|
|
270
|
+
if (!evidence.restartedScenes.includes(scene)) {
|
|
268
271
|
issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
|
|
272
|
+
}
|
|
269
273
|
}
|
|
270
274
|
for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
|
|
271
|
-
if (!evidence.checkpoints.includes(checkpoint))
|
|
275
|
+
if (!evidence.checkpoints.includes(checkpoint)) {
|
|
272
276
|
issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
|
|
277
|
+
}
|
|
273
278
|
}
|
|
274
|
-
if (requirements.requireDestroy && !evidence.destroyed)
|
|
279
|
+
if (requirements.requireDestroy && !evidence.destroyed) {
|
|
275
280
|
issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
|
|
281
|
+
}
|
|
276
282
|
return issues;
|
|
277
283
|
}
|
|
278
284
|
function auditGameplayRun(options2, tests) {
|
|
279
285
|
const issues = [];
|
|
280
286
|
const actualContracts = tests.filter((test) => test.metadata);
|
|
281
|
-
const
|
|
287
|
+
const observedEvidence = [];
|
|
282
288
|
for (const expected of options2.contracts) {
|
|
283
289
|
const matches = actualContracts.filter(
|
|
284
290
|
(test2) => test2.metadata?.id === expected.id
|
|
@@ -294,6 +300,7 @@ function auditGameplayRun(options2, tests) {
|
|
|
294
300
|
const test = matches[0];
|
|
295
301
|
const metadata = test?.metadata;
|
|
296
302
|
if (!test || !metadata) continue;
|
|
303
|
+
if (metadata.evidence) observedEvidence.push(metadata.evidence);
|
|
297
304
|
if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
|
|
298
305
|
issues.push(
|
|
299
306
|
`contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
|
|
@@ -314,7 +321,6 @@ function auditGameplayRun(options2, tests) {
|
|
|
314
321
|
metadata.evidence
|
|
315
322
|
);
|
|
316
323
|
issues.push(...evidenceIssues);
|
|
317
|
-
if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
|
|
318
324
|
}
|
|
319
325
|
for (const test of actualContracts) {
|
|
320
326
|
const id = test.metadata?.id;
|
|
@@ -325,14 +331,15 @@ function auditGameplayRun(options2, tests) {
|
|
|
325
331
|
}
|
|
326
332
|
}
|
|
327
333
|
const registeredScenes = normalizeSet(
|
|
328
|
-
|
|
334
|
+
observedEvidence.flatMap((evidence) => evidence.registeredScenes)
|
|
329
335
|
);
|
|
330
336
|
const expectedScenes = normalizeSet(options2.scenes);
|
|
331
337
|
for (const scene of expectedScenes) {
|
|
332
|
-
if (!registeredScenes.includes(scene))
|
|
338
|
+
if (!registeredScenes.includes(scene)) {
|
|
333
339
|
issues.push(
|
|
334
|
-
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\
|
|
340
|
+
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
|
|
335
341
|
);
|
|
342
|
+
}
|
|
336
343
|
}
|
|
337
344
|
for (const scene of registeredScenes) {
|
|
338
345
|
if (!expectedScenes.includes(scene))
|
|
@@ -343,6 +350,81 @@ function auditGameplayRun(options2, tests) {
|
|
|
343
350
|
return { passed: issues.length === 0, issues: [...new Set(issues)] };
|
|
344
351
|
}
|
|
345
352
|
|
|
353
|
+
// src/gameplay-audit-reporter.ts
|
|
354
|
+
import { relative } from "path";
|
|
355
|
+
function formatGameplayIssues(issues) {
|
|
356
|
+
const groups = /* @__PURE__ */ new Map();
|
|
357
|
+
const repairKeys = /* @__PURE__ */ new Set();
|
|
358
|
+
const repairs = [];
|
|
359
|
+
const repairText = {
|
|
360
|
+
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
|
+
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
|
+
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",
|
|
364
|
+
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
|
+
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",
|
|
367
|
+
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"
|
|
371
|
+
};
|
|
372
|
+
const getCategory = (issue) => {
|
|
373
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
|
|
374
|
+
if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
|
|
375
|
+
if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
|
|
376
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
|
|
377
|
+
const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
|
|
378
|
+
return { category: "transition", key: `transition:${target}` };
|
|
379
|
+
}
|
|
380
|
+
if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
|
|
381
|
+
const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
382
|
+
return { category: "visit", key: `visit:${scene}` };
|
|
383
|
+
}
|
|
384
|
+
if (issue.includes("\u672A\u91CD\u542F Scene")) {
|
|
385
|
+
const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
386
|
+
return { category: "restart", key: `restart:${scene}` };
|
|
387
|
+
}
|
|
388
|
+
if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
|
|
389
|
+
if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
|
|
390
|
+
if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
|
|
391
|
+
const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
392
|
+
return { category: "registry", key: `registry:${scene}` };
|
|
393
|
+
}
|
|
394
|
+
if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
|
|
395
|
+
if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
|
|
396
|
+
return { category: "other", key: `other:${issue}` };
|
|
397
|
+
};
|
|
398
|
+
for (const rawIssue of [...new Set(issues)]) {
|
|
399
|
+
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");
|
|
401
|
+
const id = compact.match(/contract "([^"]+)"/)?.[1];
|
|
402
|
+
const { category, key } = getCategory(compact);
|
|
403
|
+
const group = groups.get(key);
|
|
404
|
+
if (group) {
|
|
405
|
+
if (id && !group.ids.includes(id)) group.ids.push(id);
|
|
406
|
+
} else {
|
|
407
|
+
groups.set(key, { text: readable, ids: id ? [id] : [], key });
|
|
408
|
+
}
|
|
409
|
+
if (repairText[category] && !repairKeys.has(category)) {
|
|
410
|
+
repairKeys.add(category);
|
|
411
|
+
repairs.push(repairText[category]);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const grouped = [...groups.values()];
|
|
415
|
+
const visible = grouped.slice(0, 8).map((group) => {
|
|
416
|
+
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`;
|
|
418
|
+
});
|
|
419
|
+
return {
|
|
420
|
+
issues: visible.map(
|
|
421
|
+
(issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
|
|
422
|
+
),
|
|
423
|
+
repairs: repairs.slice(0, 8),
|
|
424
|
+
omitted: Math.max(0, grouped.length - visible.length)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
346
428
|
// src/lint/gameplay-audit.test.ts
|
|
347
429
|
var options = {
|
|
348
430
|
mode: "interactive",
|
|
@@ -602,6 +684,7 @@ describe("gameplay audit", () => {
|
|
|
602
684
|
evidence.destroyed = false;
|
|
603
685
|
evidence.registeredScenes.push("DebugScene");
|
|
604
686
|
const test = createPassingTest();
|
|
687
|
+
if (!test.metadata) throw new Error("expected gameplay metadata");
|
|
605
688
|
test.metadata.evidence = evidence;
|
|
606
689
|
const result = auditGameplayRun(options, [test]);
|
|
607
690
|
expect(result.passed).toBe(false);
|
|
@@ -613,8 +696,24 @@ describe("gameplay audit", () => {
|
|
|
613
696
|
])
|
|
614
697
|
);
|
|
615
698
|
});
|
|
699
|
+
it("\u4FDD\u7559\u5931\u8D25 host \u5DF2\u6CE8\u518C\u7684 Scene\uFF0C\u907F\u514D checkpoint \u7F3A\u53E3\u7EA7\u8054\u6210\u6CE8\u518C\u9519\u8BEF", () => {
|
|
700
|
+
const test = createPassingTest();
|
|
701
|
+
if (!test.metadata?.evidence) throw new Error("expected gameplay evidence");
|
|
702
|
+
test.metadata.evidence.checkpoints = ["progress"];
|
|
703
|
+
const result = auditGameplayRun(options, [test]);
|
|
704
|
+
expect(result.issues).toContain(
|
|
705
|
+
'contract "gameplay.main-flow" \u7F3A\u5C11 checkpoint "recovery"\u3002'
|
|
706
|
+
);
|
|
707
|
+
expect(result.issues).not.toContain(
|
|
708
|
+
'\u751F\u4EA7 Scene "IndexScene" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002'
|
|
709
|
+
);
|
|
710
|
+
expect(result.issues).not.toContain(
|
|
711
|
+
'\u751F\u4EA7 Scene "BattleScene" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002'
|
|
712
|
+
);
|
|
713
|
+
});
|
|
616
714
|
it("\u62D2\u7EDD\u901A\u8FC7\u5951\u7EA6\u7684 host registry \u4E2D\u51FA\u73B0\u6E05\u5355\u5916 Scene", () => {
|
|
617
715
|
const test = createPassingTest();
|
|
716
|
+
if (!test.metadata?.evidence) throw new Error("expected gameplay evidence");
|
|
618
717
|
test.metadata.evidence.registeredScenes.push("DebugScene");
|
|
619
718
|
expect(auditGameplayRun(options, [test]).issues).toContain(
|
|
620
719
|
'host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "DebugScene"\u3002'
|
|
@@ -647,4 +746,28 @@ describe("gameplay audit", () => {
|
|
|
647
746
|
)
|
|
648
747
|
).toContain('contract "arcade-physics" \u672A\u63A8\u8FDB Arcade Physics\u3002');
|
|
649
748
|
});
|
|
749
|
+
it("\u5408\u5E76\u76F8\u540C\u4FEE\u590D\u7C7B\u578B\u4F46\u4FDD\u7559\u4E0D\u540C Scene \u8F6C\u573A\u76EE\u6807", () => {
|
|
750
|
+
const result = formatGameplayIssues([
|
|
751
|
+
'contract "Menu.start" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002',
|
|
752
|
+
'contract "Battle.start" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002',
|
|
753
|
+
'contract "Menu.to-battle" \u672A\u8BB0\u5F55\u5230 BattleScene \u7684 Scene \u8F6C\u573A\u3002',
|
|
754
|
+
'contract "Battle.to-result" \u672A\u8BB0\u5F55\u5230 ResultScene \u7684 Scene \u8F6C\u573A\u3002',
|
|
755
|
+
'contract "Menu.visit-result" \u672A\u8BBF\u95EE Scene "ResultScene"\u3002',
|
|
756
|
+
'contract "Battle.visit-result" \u672A\u8BBF\u95EE Scene "ResultScene"\u3002'
|
|
757
|
+
]);
|
|
758
|
+
expect(result.issues).toHaveLength(4);
|
|
759
|
+
expect(result.repairs).toHaveLength(3);
|
|
760
|
+
expect(result.repairs[0]).toContain("\u8F93\u5165");
|
|
761
|
+
expect(result.repairs[1]).toContain("\u8F6C\u573A");
|
|
762
|
+
});
|
|
763
|
+
it("\u5C06\u591A\u68C0\u67E5\u9879\u7684\u7F3A\u53E3\u9650\u5236\u4E3A\u95EE\u9898\u7EC4\u800C\u4E0D\u662F\u539F\u59CB\u65E5\u5FD7\u884C", () => {
|
|
764
|
+
const issues = Array.from(
|
|
765
|
+
{ length: 20 },
|
|
766
|
+
(_, index) => `contract "Scene${index}.boot" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`
|
|
767
|
+
);
|
|
768
|
+
const result = formatGameplayIssues(issues);
|
|
769
|
+
expect(result.issues).toHaveLength(1);
|
|
770
|
+
expect(result.repairs).toHaveLength(1);
|
|
771
|
+
expect(result.omitted).toBe(0);
|
|
772
|
+
});
|
|
650
773
|
});
|
|
@@ -1188,6 +1188,7 @@ describe("gameplay contract lifecycle integration", () => {
|
|
|
1188
1188
|
const { callbacks, context } = createTestContext();
|
|
1189
1189
|
const contract = registerGameplayContract(context, {
|
|
1190
1190
|
id: "InputScene.hit",
|
|
1191
|
+
kind: "playthrough",
|
|
1191
1192
|
requireInput: true,
|
|
1192
1193
|
requireFrameAdvance: true
|
|
1193
1194
|
});
|
|
@@ -1216,6 +1217,7 @@ describe("gameplay contract lifecycle integration", () => {
|
|
|
1216
1217
|
const { callbacks, context } = createTestContext();
|
|
1217
1218
|
const contract = registerGameplayContract(context, {
|
|
1218
1219
|
id: "InputScene.missing-input",
|
|
1220
|
+
kind: "playthrough",
|
|
1219
1221
|
requireInput: true
|
|
1220
1222
|
});
|
|
1221
1223
|
const host = await createHeadlessGame(new InputScene());
|
|
@@ -253,12 +253,15 @@ function auditGameplayCollection(options, file, tests) {
|
|
|
253
253
|
function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
254
254
|
const issues = [];
|
|
255
255
|
const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
|
|
256
|
-
if (requirements.requireInput && inputEvents === 0)
|
|
256
|
+
if (requirements.requireInput && inputEvents === 0) {
|
|
257
257
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
|
|
258
|
-
|
|
258
|
+
}
|
|
259
|
+
if (requirements.requireFrameAdvance && evidence.frames === 0) {
|
|
259
260
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
|
|
260
|
-
|
|
261
|
+
}
|
|
262
|
+
if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
|
|
261
263
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
|
|
264
|
+
}
|
|
262
265
|
for (const target of normalizeList(requirements.requireTransition)) {
|
|
263
266
|
if (!evidence.transitions.some((transition) => transition.to === target)) {
|
|
264
267
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
|
|
@@ -269,21 +272,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
|
269
272
|
issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
|
|
270
273
|
}
|
|
271
274
|
for (const scene of normalizeList(requirements.requireRestart)) {
|
|
272
|
-
if (!evidence.restartedScenes.includes(scene))
|
|
275
|
+
if (!evidence.restartedScenes.includes(scene)) {
|
|
273
276
|
issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
|
|
277
|
+
}
|
|
274
278
|
}
|
|
275
279
|
for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
|
|
276
|
-
if (!evidence.checkpoints.includes(checkpoint))
|
|
280
|
+
if (!evidence.checkpoints.includes(checkpoint)) {
|
|
277
281
|
issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
|
|
282
|
+
}
|
|
278
283
|
}
|
|
279
|
-
if (requirements.requireDestroy && !evidence.destroyed)
|
|
284
|
+
if (requirements.requireDestroy && !evidence.destroyed) {
|
|
280
285
|
issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
|
|
286
|
+
}
|
|
281
287
|
return issues;
|
|
282
288
|
}
|
|
283
289
|
function auditGameplayRun(options, tests) {
|
|
284
290
|
const issues = [];
|
|
285
291
|
const actualContracts = tests.filter((test) => test.metadata);
|
|
286
|
-
const
|
|
292
|
+
const observedEvidence = [];
|
|
287
293
|
for (const expected of options.contracts) {
|
|
288
294
|
const matches = actualContracts.filter(
|
|
289
295
|
(test2) => test2.metadata?.id === expected.id
|
|
@@ -299,6 +305,7 @@ function auditGameplayRun(options, tests) {
|
|
|
299
305
|
const test = matches[0];
|
|
300
306
|
const metadata = test?.metadata;
|
|
301
307
|
if (!test || !metadata) continue;
|
|
308
|
+
if (metadata.evidence) observedEvidence.push(metadata.evidence);
|
|
302
309
|
if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
|
|
303
310
|
issues.push(
|
|
304
311
|
`contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
|
|
@@ -319,7 +326,6 @@ function auditGameplayRun(options, tests) {
|
|
|
319
326
|
metadata.evidence
|
|
320
327
|
);
|
|
321
328
|
issues.push(...evidenceIssues);
|
|
322
|
-
if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
|
|
323
329
|
}
|
|
324
330
|
for (const test of actualContracts) {
|
|
325
331
|
const id = test.metadata?.id;
|
|
@@ -330,14 +336,15 @@ function auditGameplayRun(options, tests) {
|
|
|
330
336
|
}
|
|
331
337
|
}
|
|
332
338
|
const registeredScenes = normalizeSet(
|
|
333
|
-
|
|
339
|
+
observedEvidence.flatMap((evidence) => evidence.registeredScenes)
|
|
334
340
|
);
|
|
335
341
|
const expectedScenes = normalizeSet(options.scenes);
|
|
336
342
|
for (const scene of expectedScenes) {
|
|
337
|
-
if (!registeredScenes.includes(scene))
|
|
343
|
+
if (!registeredScenes.includes(scene)) {
|
|
338
344
|
issues.push(
|
|
339
|
-
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\
|
|
345
|
+
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
|
|
340
346
|
);
|
|
347
|
+
}
|
|
341
348
|
}
|
|
342
349
|
for (const scene of registeredScenes) {
|
|
343
350
|
if (!expectedScenes.includes(scene))
|
|
@@ -367,11 +374,96 @@ function toAuditTest(test) {
|
|
|
367
374
|
metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
|
|
368
375
|
};
|
|
369
376
|
}
|
|
377
|
+
function formatGameplayIssues(issues) {
|
|
378
|
+
const groups = /* @__PURE__ */ new Map();
|
|
379
|
+
const repairKeys = /* @__PURE__ */ new Set();
|
|
380
|
+
const repairs = [];
|
|
381
|
+
const repairText = {
|
|
382
|
+
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
|
+
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
|
+
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",
|
|
386
|
+
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
|
+
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",
|
|
389
|
+
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"
|
|
393
|
+
};
|
|
394
|
+
const getCategory = (issue) => {
|
|
395
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
|
|
396
|
+
if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
|
|
397
|
+
if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
|
|
398
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
|
|
399
|
+
const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
|
|
400
|
+
return { category: "transition", key: `transition:${target}` };
|
|
401
|
+
}
|
|
402
|
+
if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
|
|
403
|
+
const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
404
|
+
return { category: "visit", key: `visit:${scene}` };
|
|
405
|
+
}
|
|
406
|
+
if (issue.includes("\u672A\u91CD\u542F Scene")) {
|
|
407
|
+
const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
408
|
+
return { category: "restart", key: `restart:${scene}` };
|
|
409
|
+
}
|
|
410
|
+
if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
|
|
411
|
+
if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
|
|
412
|
+
if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
|
|
413
|
+
const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
414
|
+
return { category: "registry", key: `registry:${scene}` };
|
|
415
|
+
}
|
|
416
|
+
if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
|
|
417
|
+
if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
|
|
418
|
+
return { category: "other", key: `other:${issue}` };
|
|
419
|
+
};
|
|
420
|
+
for (const rawIssue of [...new Set(issues)]) {
|
|
421
|
+
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");
|
|
423
|
+
const id = compact.match(/contract "([^"]+)"/)?.[1];
|
|
424
|
+
const { category, key } = getCategory(compact);
|
|
425
|
+
const group = groups.get(key);
|
|
426
|
+
if (group) {
|
|
427
|
+
if (id && !group.ids.includes(id)) group.ids.push(id);
|
|
428
|
+
} else {
|
|
429
|
+
groups.set(key, { text: readable, ids: id ? [id] : [], key });
|
|
430
|
+
}
|
|
431
|
+
if (repairText[category] && !repairKeys.has(category)) {
|
|
432
|
+
repairKeys.add(category);
|
|
433
|
+
repairs.push(repairText[category]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const grouped = [...groups.values()];
|
|
437
|
+
const visible = grouped.slice(0, 8).map((group) => {
|
|
438
|
+
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`;
|
|
440
|
+
});
|
|
441
|
+
return {
|
|
442
|
+
issues: visible.map(
|
|
443
|
+
(issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
|
|
444
|
+
),
|
|
445
|
+
repairs: repairs.slice(0, 8),
|
|
446
|
+
omitted: Math.max(0, grouped.length - visible.length)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
370
449
|
function printIssues(stage, issues) {
|
|
371
450
|
if (issues.length === 0) return;
|
|
372
451
|
console.error(`
|
|
373
452
|
GAMEPLAY_AUDIT: ${stage} FAILED`);
|
|
374
|
-
|
|
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
|
+
);
|
|
456
|
+
const formatted = formatGameplayIssues(issues);
|
|
457
|
+
for (const issue of formatted.issues) console.error(`- ${issue}`);
|
|
458
|
+
if (formatted.repairs.length > 0) {
|
|
459
|
+
console.error("\u4FEE\u590D\u5EFA\u8BAE\uFF08\u540C\u7C7B\u95EE\u9898\u53EA\u5217\u4E00\u6B21\uFF09\uFF1A");
|
|
460
|
+
for (const repair of formatted.repairs) console.error(`- ${repair}`);
|
|
461
|
+
}
|
|
462
|
+
if (formatted.omitted > 0) {
|
|
463
|
+
console.error(
|
|
464
|
+
`- \u5176\u4F59 ${formatted.omitted} \u7C7B\u95EE\u9898\u5DF2\u7701\u7565\uFF1B\u5148\u4FEE\u590D\u4EE5\u4E0A\u95EE\u9898\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
375
467
|
}
|
|
376
468
|
var GameplayAuditReporter = class {
|
|
377
469
|
projectRoot;
|
|
@@ -412,21 +504,30 @@ var GameplayAuditReporter = class {
|
|
|
412
504
|
(testModule) => [...testModule.children.allTests()].map(toAuditTest)
|
|
413
505
|
);
|
|
414
506
|
const result = auditGameplayRun(this.options, tests);
|
|
415
|
-
|
|
416
|
-
|
|
507
|
+
const combinedIssues = [
|
|
508
|
+
...this.preflightIssues,
|
|
509
|
+
...result.issues
|
|
510
|
+
];
|
|
511
|
+
if (combinedIssues.length > 0) {
|
|
512
|
+
if (this.preflightIssues.size > 0) {
|
|
513
|
+
console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
|
|
514
|
+
}
|
|
515
|
+
printIssues("FINAL", [...new Set(combinedIssues)]);
|
|
417
516
|
process.exitCode = 1;
|
|
418
517
|
} else {
|
|
419
518
|
console.log(
|
|
420
519
|
`
|
|
421
520
|
GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
|
|
422
521
|
);
|
|
522
|
+
console.log(
|
|
523
|
+
"GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
|
|
524
|
+
);
|
|
423
525
|
}
|
|
424
526
|
}
|
|
425
527
|
/** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
|
|
426
528
|
reportPreflightIssue(issue) {
|
|
427
529
|
if (this.preflightIssues.has(issue)) return;
|
|
428
530
|
this.preflightIssues.add(issue);
|
|
429
|
-
printIssues("PREFLIGHT", [issue]);
|
|
430
531
|
process.exitCode = 1;
|
|
431
532
|
}
|
|
432
533
|
};
|
|
@@ -476,7 +577,9 @@ function defineGameVitestConfig(options) {
|
|
|
476
577
|
...options.additionalSetupFiles ?? []
|
|
477
578
|
],
|
|
478
579
|
reporters: [
|
|
479
|
-
|
|
580
|
+
// Vitest's minimal reporter is optimized for AI output: passing tests
|
|
581
|
+
// stay quiet while failed tests retain their useful error message.
|
|
582
|
+
"minimal",
|
|
480
583
|
new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
|
|
481
584
|
],
|
|
482
585
|
restoreMocks: true,
|
|
@@ -488,7 +591,9 @@ function defineGameVitestConfig(options) {
|
|
|
488
591
|
"src/scenes/**/*.d.ts",
|
|
489
592
|
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
490
593
|
],
|
|
491
|
-
|
|
594
|
+
// Per-file threshold errors still identify omitted Scene files; the
|
|
595
|
+
// summary avoids printing a large coverage table on every successful run.
|
|
596
|
+
reporter: ["text-summary"],
|
|
492
597
|
thresholds: {
|
|
493
598
|
perFile: true,
|
|
494
599
|
lines: 1,
|
|
@@ -535,7 +640,7 @@ describe("defineGameVitestConfig", () => {
|
|
|
535
640
|
environment: "jsdom",
|
|
536
641
|
environmentOptions: { jsdom: { url: "http://localhost/" } },
|
|
537
642
|
setupFiles: ["miaoda-game-devkit/vitest-setup"],
|
|
538
|
-
reporters: ["
|
|
643
|
+
reporters: ["minimal", expect.any(Object)],
|
|
539
644
|
restoreMocks: true,
|
|
540
645
|
clearMocks: true,
|
|
541
646
|
coverage: {
|
|
@@ -545,7 +650,7 @@ describe("defineGameVitestConfig", () => {
|
|
|
545
650
|
"src/scenes/**/*.d.ts",
|
|
546
651
|
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
547
652
|
],
|
|
548
|
-
reporter: ["text"],
|
|
653
|
+
reporter: ["text-summary"],
|
|
549
654
|
thresholds: {
|
|
550
655
|
perFile: true,
|
|
551
656
|
lines: 1,
|
package/dist/vitest-config.js
CHANGED
|
@@ -273,12 +273,15 @@ function auditGameplayCollection(options, file, tests) {
|
|
|
273
273
|
function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
274
274
|
const issues = [];
|
|
275
275
|
const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
|
|
276
|
-
if (requirements.requireInput && inputEvents === 0)
|
|
276
|
+
if (requirements.requireInput && inputEvents === 0) {
|
|
277
277
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
|
|
278
|
-
|
|
278
|
+
}
|
|
279
|
+
if (requirements.requireFrameAdvance && evidence.frames === 0) {
|
|
279
280
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
|
|
280
|
-
|
|
281
|
+
}
|
|
282
|
+
if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
|
|
281
283
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
|
|
284
|
+
}
|
|
282
285
|
for (const target of normalizeList(requirements.requireTransition)) {
|
|
283
286
|
if (!evidence.transitions.some((transition) => transition.to === target)) {
|
|
284
287
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
|
|
@@ -289,21 +292,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
|
289
292
|
issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
|
|
290
293
|
}
|
|
291
294
|
for (const scene of normalizeList(requirements.requireRestart)) {
|
|
292
|
-
if (!evidence.restartedScenes.includes(scene))
|
|
295
|
+
if (!evidence.restartedScenes.includes(scene)) {
|
|
293
296
|
issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
|
|
297
|
+
}
|
|
294
298
|
}
|
|
295
299
|
for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
|
|
296
|
-
if (!evidence.checkpoints.includes(checkpoint))
|
|
300
|
+
if (!evidence.checkpoints.includes(checkpoint)) {
|
|
297
301
|
issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
|
|
302
|
+
}
|
|
298
303
|
}
|
|
299
|
-
if (requirements.requireDestroy && !evidence.destroyed)
|
|
304
|
+
if (requirements.requireDestroy && !evidence.destroyed) {
|
|
300
305
|
issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
|
|
306
|
+
}
|
|
301
307
|
return issues;
|
|
302
308
|
}
|
|
303
309
|
function auditGameplayRun(options, tests) {
|
|
304
310
|
const issues = [];
|
|
305
311
|
const actualContracts = tests.filter((test) => test.metadata);
|
|
306
|
-
const
|
|
312
|
+
const observedEvidence = [];
|
|
307
313
|
for (const expected of options.contracts) {
|
|
308
314
|
const matches = actualContracts.filter(
|
|
309
315
|
(test2) => test2.metadata?.id === expected.id
|
|
@@ -319,6 +325,7 @@ function auditGameplayRun(options, tests) {
|
|
|
319
325
|
const test = matches[0];
|
|
320
326
|
const metadata = test?.metadata;
|
|
321
327
|
if (!test || !metadata) continue;
|
|
328
|
+
if (metadata.evidence) observedEvidence.push(metadata.evidence);
|
|
322
329
|
if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
|
|
323
330
|
issues.push(
|
|
324
331
|
`contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
|
|
@@ -339,7 +346,6 @@ function auditGameplayRun(options, tests) {
|
|
|
339
346
|
metadata.evidence
|
|
340
347
|
);
|
|
341
348
|
issues.push(...evidenceIssues);
|
|
342
|
-
if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
|
|
343
349
|
}
|
|
344
350
|
for (const test of actualContracts) {
|
|
345
351
|
const id = test.metadata?.id;
|
|
@@ -350,14 +356,15 @@ function auditGameplayRun(options, tests) {
|
|
|
350
356
|
}
|
|
351
357
|
}
|
|
352
358
|
const registeredScenes = normalizeSet(
|
|
353
|
-
|
|
359
|
+
observedEvidence.flatMap((evidence) => evidence.registeredScenes)
|
|
354
360
|
);
|
|
355
361
|
const expectedScenes = normalizeSet(options.scenes);
|
|
356
362
|
for (const scene of expectedScenes) {
|
|
357
|
-
if (!registeredScenes.includes(scene))
|
|
363
|
+
if (!registeredScenes.includes(scene)) {
|
|
358
364
|
issues.push(
|
|
359
|
-
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\
|
|
365
|
+
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
|
|
360
366
|
);
|
|
367
|
+
}
|
|
361
368
|
}
|
|
362
369
|
for (const scene of registeredScenes) {
|
|
363
370
|
if (!expectedScenes.includes(scene))
|
|
@@ -387,11 +394,96 @@ function toAuditTest(test) {
|
|
|
387
394
|
metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
|
|
388
395
|
};
|
|
389
396
|
}
|
|
397
|
+
function formatGameplayIssues(issues) {
|
|
398
|
+
const groups = /* @__PURE__ */ new Map();
|
|
399
|
+
const repairKeys = /* @__PURE__ */ new Set();
|
|
400
|
+
const repairs = [];
|
|
401
|
+
const repairText = {
|
|
402
|
+
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
|
+
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
|
+
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",
|
|
406
|
+
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
|
+
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",
|
|
409
|
+
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"
|
|
413
|
+
};
|
|
414
|
+
const getCategory = (issue) => {
|
|
415
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
|
|
416
|
+
if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
|
|
417
|
+
if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
|
|
418
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
|
|
419
|
+
const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
|
|
420
|
+
return { category: "transition", key: `transition:${target}` };
|
|
421
|
+
}
|
|
422
|
+
if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
|
|
423
|
+
const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
424
|
+
return { category: "visit", key: `visit:${scene}` };
|
|
425
|
+
}
|
|
426
|
+
if (issue.includes("\u672A\u91CD\u542F Scene")) {
|
|
427
|
+
const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
428
|
+
return { category: "restart", key: `restart:${scene}` };
|
|
429
|
+
}
|
|
430
|
+
if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
|
|
431
|
+
if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
|
|
432
|
+
if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
|
|
433
|
+
const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
434
|
+
return { category: "registry", key: `registry:${scene}` };
|
|
435
|
+
}
|
|
436
|
+
if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
|
|
437
|
+
if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
|
|
438
|
+
return { category: "other", key: `other:${issue}` };
|
|
439
|
+
};
|
|
440
|
+
for (const rawIssue of [...new Set(issues)]) {
|
|
441
|
+
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");
|
|
443
|
+
const id = compact.match(/contract "([^"]+)"/)?.[1];
|
|
444
|
+
const { category, key } = getCategory(compact);
|
|
445
|
+
const group = groups.get(key);
|
|
446
|
+
if (group) {
|
|
447
|
+
if (id && !group.ids.includes(id)) group.ids.push(id);
|
|
448
|
+
} else {
|
|
449
|
+
groups.set(key, { text: readable, ids: id ? [id] : [], key });
|
|
450
|
+
}
|
|
451
|
+
if (repairText[category] && !repairKeys.has(category)) {
|
|
452
|
+
repairKeys.add(category);
|
|
453
|
+
repairs.push(repairText[category]);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const grouped = [...groups.values()];
|
|
457
|
+
const visible = grouped.slice(0, 8).map((group) => {
|
|
458
|
+
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`;
|
|
460
|
+
});
|
|
461
|
+
return {
|
|
462
|
+
issues: visible.map(
|
|
463
|
+
(issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
|
|
464
|
+
),
|
|
465
|
+
repairs: repairs.slice(0, 8),
|
|
466
|
+
omitted: Math.max(0, grouped.length - visible.length)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
390
469
|
function printIssues(stage, issues) {
|
|
391
470
|
if (issues.length === 0) return;
|
|
392
471
|
console.error(`
|
|
393
472
|
GAMEPLAY_AUDIT: ${stage} FAILED`);
|
|
394
|
-
|
|
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
|
+
);
|
|
476
|
+
const formatted = formatGameplayIssues(issues);
|
|
477
|
+
for (const issue of formatted.issues) console.error(`- ${issue}`);
|
|
478
|
+
if (formatted.repairs.length > 0) {
|
|
479
|
+
console.error("\u4FEE\u590D\u5EFA\u8BAE\uFF08\u540C\u7C7B\u95EE\u9898\u53EA\u5217\u4E00\u6B21\uFF09\uFF1A");
|
|
480
|
+
for (const repair of formatted.repairs) console.error(`- ${repair}`);
|
|
481
|
+
}
|
|
482
|
+
if (formatted.omitted > 0) {
|
|
483
|
+
console.error(
|
|
484
|
+
`- \u5176\u4F59 ${formatted.omitted} \u7C7B\u95EE\u9898\u5DF2\u7701\u7565\uFF1B\u5148\u4FEE\u590D\u4EE5\u4E0A\u95EE\u9898\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
395
487
|
}
|
|
396
488
|
var GameplayAuditReporter = class {
|
|
397
489
|
projectRoot;
|
|
@@ -432,21 +524,30 @@ var GameplayAuditReporter = class {
|
|
|
432
524
|
(testModule) => [...testModule.children.allTests()].map(toAuditTest)
|
|
433
525
|
);
|
|
434
526
|
const result = auditGameplayRun(this.options, tests);
|
|
435
|
-
|
|
436
|
-
|
|
527
|
+
const combinedIssues = [
|
|
528
|
+
...this.preflightIssues,
|
|
529
|
+
...result.issues
|
|
530
|
+
];
|
|
531
|
+
if (combinedIssues.length > 0) {
|
|
532
|
+
if (this.preflightIssues.size > 0) {
|
|
533
|
+
console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
|
|
534
|
+
}
|
|
535
|
+
printIssues("FINAL", [...new Set(combinedIssues)]);
|
|
437
536
|
process.exitCode = 1;
|
|
438
537
|
} else {
|
|
439
538
|
console.log(
|
|
440
539
|
`
|
|
441
540
|
GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
|
|
442
541
|
);
|
|
542
|
+
console.log(
|
|
543
|
+
"GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
|
|
544
|
+
);
|
|
443
545
|
}
|
|
444
546
|
}
|
|
445
547
|
/** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
|
|
446
548
|
reportPreflightIssue(issue) {
|
|
447
549
|
if (this.preflightIssues.has(issue)) return;
|
|
448
550
|
this.preflightIssues.add(issue);
|
|
449
|
-
printIssues("PREFLIGHT", [issue]);
|
|
450
551
|
process.exitCode = 1;
|
|
451
552
|
}
|
|
452
553
|
};
|
|
@@ -496,7 +597,9 @@ function defineGameVitestConfig(options) {
|
|
|
496
597
|
...options.additionalSetupFiles ?? []
|
|
497
598
|
],
|
|
498
599
|
reporters: [
|
|
499
|
-
|
|
600
|
+
// Vitest's minimal reporter is optimized for AI output: passing tests
|
|
601
|
+
// stay quiet while failed tests retain their useful error message.
|
|
602
|
+
"minimal",
|
|
500
603
|
new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
|
|
501
604
|
],
|
|
502
605
|
restoreMocks: true,
|
|
@@ -508,7 +611,9 @@ function defineGameVitestConfig(options) {
|
|
|
508
611
|
"src/scenes/**/*.d.ts",
|
|
509
612
|
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
510
613
|
],
|
|
511
|
-
|
|
614
|
+
// Per-file threshold errors still identify omitted Scene files; the
|
|
615
|
+
// summary avoids printing a large coverage table on every successful run.
|
|
616
|
+
reporter: ["text-summary"],
|
|
512
617
|
thresholds: {
|
|
513
618
|
perFile: true,
|
|
514
619
|
lines: 1,
|
package/dist/vitest-config.mjs
CHANGED
|
@@ -249,12 +249,15 @@ function auditGameplayCollection(options, file, tests) {
|
|
|
249
249
|
function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
250
250
|
const issues = [];
|
|
251
251
|
const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
|
|
252
|
-
if (requirements.requireInput && inputEvents === 0)
|
|
252
|
+
if (requirements.requireInput && inputEvents === 0) {
|
|
253
253
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
|
|
254
|
-
|
|
254
|
+
}
|
|
255
|
+
if (requirements.requireFrameAdvance && evidence.frames === 0) {
|
|
255
256
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
|
|
256
|
-
|
|
257
|
+
}
|
|
258
|
+
if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
|
|
257
259
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
|
|
260
|
+
}
|
|
258
261
|
for (const target of normalizeList(requirements.requireTransition)) {
|
|
259
262
|
if (!evidence.transitions.some((transition) => transition.to === target)) {
|
|
260
263
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
|
|
@@ -265,21 +268,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
|
265
268
|
issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
|
|
266
269
|
}
|
|
267
270
|
for (const scene of normalizeList(requirements.requireRestart)) {
|
|
268
|
-
if (!evidence.restartedScenes.includes(scene))
|
|
271
|
+
if (!evidence.restartedScenes.includes(scene)) {
|
|
269
272
|
issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
|
|
273
|
+
}
|
|
270
274
|
}
|
|
271
275
|
for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
|
|
272
|
-
if (!evidence.checkpoints.includes(checkpoint))
|
|
276
|
+
if (!evidence.checkpoints.includes(checkpoint)) {
|
|
273
277
|
issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
|
|
278
|
+
}
|
|
274
279
|
}
|
|
275
|
-
if (requirements.requireDestroy && !evidence.destroyed)
|
|
280
|
+
if (requirements.requireDestroy && !evidence.destroyed) {
|
|
276
281
|
issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
|
|
282
|
+
}
|
|
277
283
|
return issues;
|
|
278
284
|
}
|
|
279
285
|
function auditGameplayRun(options, tests) {
|
|
280
286
|
const issues = [];
|
|
281
287
|
const actualContracts = tests.filter((test) => test.metadata);
|
|
282
|
-
const
|
|
288
|
+
const observedEvidence = [];
|
|
283
289
|
for (const expected of options.contracts) {
|
|
284
290
|
const matches = actualContracts.filter(
|
|
285
291
|
(test2) => test2.metadata?.id === expected.id
|
|
@@ -295,6 +301,7 @@ function auditGameplayRun(options, tests) {
|
|
|
295
301
|
const test = matches[0];
|
|
296
302
|
const metadata = test?.metadata;
|
|
297
303
|
if (!test || !metadata) continue;
|
|
304
|
+
if (metadata.evidence) observedEvidence.push(metadata.evidence);
|
|
298
305
|
if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
|
|
299
306
|
issues.push(
|
|
300
307
|
`contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
|
|
@@ -315,7 +322,6 @@ function auditGameplayRun(options, tests) {
|
|
|
315
322
|
metadata.evidence
|
|
316
323
|
);
|
|
317
324
|
issues.push(...evidenceIssues);
|
|
318
|
-
if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
|
|
319
325
|
}
|
|
320
326
|
for (const test of actualContracts) {
|
|
321
327
|
const id = test.metadata?.id;
|
|
@@ -326,14 +332,15 @@ function auditGameplayRun(options, tests) {
|
|
|
326
332
|
}
|
|
327
333
|
}
|
|
328
334
|
const registeredScenes = normalizeSet(
|
|
329
|
-
|
|
335
|
+
observedEvidence.flatMap((evidence) => evidence.registeredScenes)
|
|
330
336
|
);
|
|
331
337
|
const expectedScenes = normalizeSet(options.scenes);
|
|
332
338
|
for (const scene of expectedScenes) {
|
|
333
|
-
if (!registeredScenes.includes(scene))
|
|
339
|
+
if (!registeredScenes.includes(scene)) {
|
|
334
340
|
issues.push(
|
|
335
|
-
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\
|
|
341
|
+
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
|
|
336
342
|
);
|
|
343
|
+
}
|
|
337
344
|
}
|
|
338
345
|
for (const scene of registeredScenes) {
|
|
339
346
|
if (!expectedScenes.includes(scene))
|
|
@@ -363,11 +370,96 @@ function toAuditTest(test) {
|
|
|
363
370
|
metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
|
|
364
371
|
};
|
|
365
372
|
}
|
|
373
|
+
function formatGameplayIssues(issues) {
|
|
374
|
+
const groups = /* @__PURE__ */ new Map();
|
|
375
|
+
const repairKeys = /* @__PURE__ */ new Set();
|
|
376
|
+
const repairs = [];
|
|
377
|
+
const repairText = {
|
|
378
|
+
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
|
+
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
|
+
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",
|
|
382
|
+
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
|
+
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",
|
|
385
|
+
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"
|
|
389
|
+
};
|
|
390
|
+
const getCategory = (issue) => {
|
|
391
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
|
|
392
|
+
if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
|
|
393
|
+
if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
|
|
394
|
+
if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
|
|
395
|
+
const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
|
|
396
|
+
return { category: "transition", key: `transition:${target}` };
|
|
397
|
+
}
|
|
398
|
+
if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
|
|
399
|
+
const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
400
|
+
return { category: "visit", key: `visit:${scene}` };
|
|
401
|
+
}
|
|
402
|
+
if (issue.includes("\u672A\u91CD\u542F Scene")) {
|
|
403
|
+
const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
404
|
+
return { category: "restart", key: `restart:${scene}` };
|
|
405
|
+
}
|
|
406
|
+
if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
|
|
407
|
+
if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
|
|
408
|
+
if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
|
|
409
|
+
const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
410
|
+
return { category: "registry", key: `registry:${scene}` };
|
|
411
|
+
}
|
|
412
|
+
if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
|
|
413
|
+
if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
|
|
414
|
+
return { category: "other", key: `other:${issue}` };
|
|
415
|
+
};
|
|
416
|
+
for (const rawIssue of [...new Set(issues)]) {
|
|
417
|
+
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");
|
|
419
|
+
const id = compact.match(/contract "([^"]+)"/)?.[1];
|
|
420
|
+
const { category, key } = getCategory(compact);
|
|
421
|
+
const group = groups.get(key);
|
|
422
|
+
if (group) {
|
|
423
|
+
if (id && !group.ids.includes(id)) group.ids.push(id);
|
|
424
|
+
} else {
|
|
425
|
+
groups.set(key, { text: readable, ids: id ? [id] : [], key });
|
|
426
|
+
}
|
|
427
|
+
if (repairText[category] && !repairKeys.has(category)) {
|
|
428
|
+
repairKeys.add(category);
|
|
429
|
+
repairs.push(repairText[category]);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const grouped = [...groups.values()];
|
|
433
|
+
const visible = grouped.slice(0, 8).map((group) => {
|
|
434
|
+
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`;
|
|
436
|
+
});
|
|
437
|
+
return {
|
|
438
|
+
issues: visible.map(
|
|
439
|
+
(issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
|
|
440
|
+
),
|
|
441
|
+
repairs: repairs.slice(0, 8),
|
|
442
|
+
omitted: Math.max(0, grouped.length - visible.length)
|
|
443
|
+
};
|
|
444
|
+
}
|
|
366
445
|
function printIssues(stage, issues) {
|
|
367
446
|
if (issues.length === 0) return;
|
|
368
447
|
console.error(`
|
|
369
448
|
GAMEPLAY_AUDIT: ${stage} FAILED`);
|
|
370
|
-
|
|
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
|
+
);
|
|
452
|
+
const formatted = formatGameplayIssues(issues);
|
|
453
|
+
for (const issue of formatted.issues) console.error(`- ${issue}`);
|
|
454
|
+
if (formatted.repairs.length > 0) {
|
|
455
|
+
console.error("\u4FEE\u590D\u5EFA\u8BAE\uFF08\u540C\u7C7B\u95EE\u9898\u53EA\u5217\u4E00\u6B21\uFF09\uFF1A");
|
|
456
|
+
for (const repair of formatted.repairs) console.error(`- ${repair}`);
|
|
457
|
+
}
|
|
458
|
+
if (formatted.omitted > 0) {
|
|
459
|
+
console.error(
|
|
460
|
+
`- \u5176\u4F59 ${formatted.omitted} \u7C7B\u95EE\u9898\u5DF2\u7701\u7565\uFF1B\u5148\u4FEE\u590D\u4EE5\u4E0A\u95EE\u9898\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
371
463
|
}
|
|
372
464
|
var GameplayAuditReporter = class {
|
|
373
465
|
projectRoot;
|
|
@@ -408,21 +500,30 @@ var GameplayAuditReporter = class {
|
|
|
408
500
|
(testModule) => [...testModule.children.allTests()].map(toAuditTest)
|
|
409
501
|
);
|
|
410
502
|
const result = auditGameplayRun(this.options, tests);
|
|
411
|
-
|
|
412
|
-
|
|
503
|
+
const combinedIssues = [
|
|
504
|
+
...this.preflightIssues,
|
|
505
|
+
...result.issues
|
|
506
|
+
];
|
|
507
|
+
if (combinedIssues.length > 0) {
|
|
508
|
+
if (this.preflightIssues.size > 0) {
|
|
509
|
+
console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
|
|
510
|
+
}
|
|
511
|
+
printIssues("FINAL", [...new Set(combinedIssues)]);
|
|
413
512
|
process.exitCode = 1;
|
|
414
513
|
} else {
|
|
415
514
|
console.log(
|
|
416
515
|
`
|
|
417
516
|
GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
|
|
418
517
|
);
|
|
518
|
+
console.log(
|
|
519
|
+
"GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
|
|
520
|
+
);
|
|
419
521
|
}
|
|
420
522
|
}
|
|
421
523
|
/** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
|
|
422
524
|
reportPreflightIssue(issue) {
|
|
423
525
|
if (this.preflightIssues.has(issue)) return;
|
|
424
526
|
this.preflightIssues.add(issue);
|
|
425
|
-
printIssues("PREFLIGHT", [issue]);
|
|
426
527
|
process.exitCode = 1;
|
|
427
528
|
}
|
|
428
529
|
};
|
|
@@ -472,7 +573,9 @@ function defineGameVitestConfig(options) {
|
|
|
472
573
|
...options.additionalSetupFiles ?? []
|
|
473
574
|
],
|
|
474
575
|
reporters: [
|
|
475
|
-
|
|
576
|
+
// Vitest's minimal reporter is optimized for AI output: passing tests
|
|
577
|
+
// stay quiet while failed tests retain their useful error message.
|
|
578
|
+
"minimal",
|
|
476
579
|
new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
|
|
477
580
|
],
|
|
478
581
|
restoreMocks: true,
|
|
@@ -484,7 +587,9 @@ function defineGameVitestConfig(options) {
|
|
|
484
587
|
"src/scenes/**/*.d.ts",
|
|
485
588
|
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
486
589
|
],
|
|
487
|
-
|
|
590
|
+
// Per-file threshold errors still identify omitted Scene files; the
|
|
591
|
+
// summary avoids printing a large coverage table on every successful run.
|
|
592
|
+
reporter: ["text-summary"],
|
|
488
593
|
thresholds: {
|
|
489
594
|
perFile: true,
|
|
490
595
|
lines: 1,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miaoda-game-devkit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -43,18 +43,6 @@
|
|
|
43
43
|
"!dist/**/*.map",
|
|
44
44
|
"!dist/lint/**/*.test.*"
|
|
45
45
|
],
|
|
46
|
-
"scripts": {
|
|
47
|
-
"build": "tsup",
|
|
48
|
-
"prepack": "pnpm run build",
|
|
49
|
-
"release": "pnpm run test && clean-publish",
|
|
50
|
-
"release:dry-run": "pnpm run test && clean-publish --dry-run",
|
|
51
|
-
"release:beta": "pnpm run test && npm version prerelease --preid=beta && clean-publish --tag beta",
|
|
52
|
-
"release:patch": "pnpm run test && npm version patch && clean-publish",
|
|
53
|
-
"release:minor": "pnpm run test && npm version minor && clean-publish",
|
|
54
|
-
"release:major": "pnpm run test && npm version major && clean-publish",
|
|
55
|
-
"typecheck": "tsc --noEmit",
|
|
56
|
-
"test": "pnpm run build && ./bin/miaoda-game-lint.js --contracts-only"
|
|
57
|
-
},
|
|
58
46
|
"nx": {
|
|
59
47
|
"tags": [
|
|
60
48
|
"type:package",
|
|
@@ -94,23 +82,8 @@
|
|
|
94
82
|
"peerDependencies": {
|
|
95
83
|
"phaser": ">=4.0.0 <5"
|
|
96
84
|
},
|
|
97
|
-
"devDependencies": {
|
|
98
|
-
"@types/eslint": "^9.6.1",
|
|
99
|
-
"@types/estree": "^1.0.9",
|
|
100
|
-
"@types/node": "^24.13.3",
|
|
101
|
-
"clean-publish": "^6.0.5",
|
|
102
|
-
"phaser": "4.0.0",
|
|
103
|
-
"tsup": "^8.5.1",
|
|
104
|
-
"typescript": "^5.9.3"
|
|
105
|
-
},
|
|
106
85
|
"publishConfig": {
|
|
107
86
|
"registry": "https://registry.npmjs.org/",
|
|
108
87
|
"access": "public"
|
|
109
|
-
},
|
|
110
|
-
"clean-publish": {
|
|
111
|
-
"fields": [
|
|
112
|
-
"devDependencies",
|
|
113
|
-
"scripts"
|
|
114
|
-
]
|
|
115
88
|
}
|
|
116
89
|
}
|