miaoda-game-devkit 0.2.2 → 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/{gameplay-audit-CYqLL8gY.d.mts → gameplay-audit-XBGq_jIV.d.mts} +13 -0
- package/dist/{gameplay-audit-CYqLL8gY.d.ts → gameplay-audit-XBGq_jIV.d.ts} +13 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -0
- package/dist/index.mjs +3 -0
- package/dist/lint/gameplay-audit.contract.mjs +360 -13
- package/dist/lint/gameplay-contract.contract.mjs +5 -0
- package/dist/lint/vitest-config.contract.mjs +245 -22
- package/dist/vitest-config.d.mts +1 -1
- package/dist/vitest-config.d.ts +1 -1
- package/dist/vitest-config.js +232 -24
- package/dist/vitest-config.mjs +228 -20
- package/package.json +1 -1
package/dist/vitest-config.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import { resolve } from "path";
|
|
|
3
3
|
import { defineConfig } from "vitest/config";
|
|
4
4
|
|
|
5
5
|
// src/gameplay-audit.ts
|
|
6
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
6
8
|
function normalizeList(value) {
|
|
7
9
|
if (value === void 0) return [];
|
|
8
10
|
const values = typeof value === "string" ? [value] : value;
|
|
@@ -36,20 +38,35 @@ function createGameplayContractMetadata(contract) {
|
|
|
36
38
|
const scenes = normalizeSet(contract.scenes);
|
|
37
39
|
return {
|
|
38
40
|
id: contract.id.trim(),
|
|
41
|
+
kind: contract.kind,
|
|
39
42
|
scenes,
|
|
40
43
|
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
41
44
|
verified: false
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
|
-
function validateGameplayAuditOptions(options) {
|
|
47
|
+
function validateGameplayAuditOptions(options, projectRoot) {
|
|
45
48
|
const issues = [];
|
|
46
|
-
const scenes =
|
|
49
|
+
const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
|
|
50
|
+
const mode = options.mode ?? "interactive";
|
|
51
|
+
if (mode === "non-interactive") {
|
|
52
|
+
const justification = options.nonInteractiveJustification?.trim() ?? "";
|
|
53
|
+
if (justification.length < 20) {
|
|
54
|
+
issues.push(
|
|
55
|
+
"non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (mode !== "interactive" && mode !== "non-interactive") {
|
|
60
|
+
issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
|
|
61
|
+
}
|
|
47
62
|
if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
48
63
|
if (scenes.length !== options.scenes.length) {
|
|
49
64
|
issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
|
|
50
65
|
}
|
|
51
66
|
const contractIds = /* @__PURE__ */ new Set();
|
|
52
67
|
const coveredScenes = /* @__PURE__ */ new Set();
|
|
68
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
69
|
+
const invalidKindContracts = [];
|
|
53
70
|
for (const contract of options.contracts) {
|
|
54
71
|
const id = contract.id.trim();
|
|
55
72
|
if (!id) {
|
|
@@ -68,6 +85,27 @@ function validateGameplayAuditOptions(options) {
|
|
|
68
85
|
);
|
|
69
86
|
}
|
|
70
87
|
const contractScenes = normalizeSet(contract.scenes);
|
|
88
|
+
if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
|
|
89
|
+
invalidKindContracts.push(id || "<empty>");
|
|
90
|
+
} else {
|
|
91
|
+
const list = kinds.get(contract.kind) ?? [];
|
|
92
|
+
list.push(contract);
|
|
93
|
+
kinds.set(contract.kind, list);
|
|
94
|
+
}
|
|
95
|
+
if (contract.kind === "playthrough") {
|
|
96
|
+
if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
97
|
+
if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
98
|
+
if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
|
|
99
|
+
}
|
|
100
|
+
if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
|
|
101
|
+
issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
|
|
102
|
+
}
|
|
103
|
+
if (contract.kind === "recovery") {
|
|
104
|
+
if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
105
|
+
if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
106
|
+
if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
|
|
107
|
+
if (normalizeList(contract.requireCheckpoint).length < 2) issues.push(`recovery contract "${id}" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002`);
|
|
108
|
+
}
|
|
71
109
|
if (contractScenes.length === 0) {
|
|
72
110
|
issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
|
|
73
111
|
}
|
|
@@ -80,6 +118,11 @@ function validateGameplayAuditOptions(options) {
|
|
|
80
118
|
}
|
|
81
119
|
}
|
|
82
120
|
}
|
|
121
|
+
if (invalidKindContracts.length > 0) {
|
|
122
|
+
issues.push(
|
|
123
|
+
`\u4EE5\u4E0B contract \u7F3A\u5C11\u6709\u6548 kind\uFF1A${invalidKindContracts.map((id) => `"${id}"`).join("\u3001")}\u3002\u8BF7\u4E3A\u6BCF\u4E2A contract \u8BBE\u7F6E kind: "boot"\u3001"playthrough"\u3001"transition" \u6216 "recovery"\u3002`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
83
126
|
for (const scene of scenes) {
|
|
84
127
|
if (!coveredScenes.has(scene)) {
|
|
85
128
|
issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
|
|
@@ -87,11 +130,71 @@ function validateGameplayAuditOptions(options) {
|
|
|
87
130
|
}
|
|
88
131
|
if (options.contracts.length === 0)
|
|
89
132
|
issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
133
|
+
if (mode === "interactive") {
|
|
134
|
+
const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
|
|
135
|
+
const hasInput = options.contracts.some((contract) => contract.requireInput);
|
|
136
|
+
const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
|
|
137
|
+
if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
|
|
138
|
+
issues.push(
|
|
139
|
+
'\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A kind: "playthrough" contract \u5FC5\u987B\u540C\u65F6\u58F0\u660E requireInput: true\u3001requireFrameAdvance: true \u548C requireCheckpoint: ["progress"]\u3002'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const entryScene = scenes[0];
|
|
143
|
+
for (const scene of scenes.slice(1)) {
|
|
144
|
+
const transitioned = options.contracts.some(
|
|
145
|
+
(contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
|
|
146
|
+
);
|
|
147
|
+
if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
|
|
148
|
+
}
|
|
149
|
+
if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
|
|
150
|
+
issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
|
|
151
|
+
}
|
|
152
|
+
const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
|
|
153
|
+
if (restartSignal && options.flows?.restart !== true) {
|
|
154
|
+
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF1B\u5FC5\u987B\u58F0\u660E flows.restart: true \u5E76\u63D0\u4F9B kind: recovery contract\u3002");
|
|
155
|
+
} else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
|
|
156
|
+
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
157
|
+
} else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
|
|
158
|
+
issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
90
161
|
return issues;
|
|
91
162
|
}
|
|
163
|
+
function containsProductionRestart(projectRoot) {
|
|
164
|
+
const sceneRoot = join(projectRoot, "src", "scenes");
|
|
165
|
+
const visit = (directory) => {
|
|
166
|
+
let entries;
|
|
167
|
+
try {
|
|
168
|
+
entries = readdirSync(directory);
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const path = join(directory, entry);
|
|
174
|
+
let stats;
|
|
175
|
+
try {
|
|
176
|
+
stats = statSync(path);
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (stats.isDirectory() && visit(path)) return true;
|
|
181
|
+
if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
|
|
182
|
+
try {
|
|
183
|
+
const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
184
|
+
if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
};
|
|
190
|
+
return visit(sceneRoot);
|
|
191
|
+
}
|
|
92
192
|
function getDefinitionIssues(expected, actual) {
|
|
93
193
|
const issues = [];
|
|
94
194
|
const expectedMetadata = createGameplayContractMetadata(expected);
|
|
195
|
+
if (expectedMetadata.kind !== actual.kind) {
|
|
196
|
+
issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
|
|
197
|
+
}
|
|
95
198
|
if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
|
|
96
199
|
issues.push(
|
|
97
200
|
`contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
@@ -146,12 +249,15 @@ function auditGameplayCollection(options, file, tests) {
|
|
|
146
249
|
function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
147
250
|
const issues = [];
|
|
148
251
|
const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
|
|
149
|
-
if (requirements.requireInput && inputEvents === 0)
|
|
252
|
+
if (requirements.requireInput && inputEvents === 0) {
|
|
150
253
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
|
|
151
|
-
|
|
254
|
+
}
|
|
255
|
+
if (requirements.requireFrameAdvance && evidence.frames === 0) {
|
|
152
256
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
|
|
153
|
-
|
|
257
|
+
}
|
|
258
|
+
if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
|
|
154
259
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
|
|
260
|
+
}
|
|
155
261
|
for (const target of normalizeList(requirements.requireTransition)) {
|
|
156
262
|
if (!evidence.transitions.some((transition) => transition.to === target)) {
|
|
157
263
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
|
|
@@ -162,21 +268,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
|
162
268
|
issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
|
|
163
269
|
}
|
|
164
270
|
for (const scene of normalizeList(requirements.requireRestart)) {
|
|
165
|
-
if (!evidence.restartedScenes.includes(scene))
|
|
271
|
+
if (!evidence.restartedScenes.includes(scene)) {
|
|
166
272
|
issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
|
|
273
|
+
}
|
|
167
274
|
}
|
|
168
275
|
for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
|
|
169
|
-
if (!evidence.checkpoints.includes(checkpoint))
|
|
276
|
+
if (!evidence.checkpoints.includes(checkpoint)) {
|
|
170
277
|
issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
|
|
278
|
+
}
|
|
171
279
|
}
|
|
172
|
-
if (requirements.requireDestroy && !evidence.destroyed)
|
|
280
|
+
if (requirements.requireDestroy && !evidence.destroyed) {
|
|
173
281
|
issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
|
|
282
|
+
}
|
|
174
283
|
return issues;
|
|
175
284
|
}
|
|
176
285
|
function auditGameplayRun(options, tests) {
|
|
177
286
|
const issues = [];
|
|
178
287
|
const actualContracts = tests.filter((test) => test.metadata);
|
|
179
|
-
const
|
|
288
|
+
const observedEvidence = [];
|
|
180
289
|
for (const expected of options.contracts) {
|
|
181
290
|
const matches = actualContracts.filter(
|
|
182
291
|
(test2) => test2.metadata?.id === expected.id
|
|
@@ -192,6 +301,7 @@ function auditGameplayRun(options, tests) {
|
|
|
192
301
|
const test = matches[0];
|
|
193
302
|
const metadata = test?.metadata;
|
|
194
303
|
if (!test || !metadata) continue;
|
|
304
|
+
if (metadata.evidence) observedEvidence.push(metadata.evidence);
|
|
195
305
|
if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
|
|
196
306
|
issues.push(
|
|
197
307
|
`contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
|
|
@@ -212,7 +322,6 @@ function auditGameplayRun(options, tests) {
|
|
|
212
322
|
metadata.evidence
|
|
213
323
|
);
|
|
214
324
|
issues.push(...evidenceIssues);
|
|
215
|
-
if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
|
|
216
325
|
}
|
|
217
326
|
for (const test of actualContracts) {
|
|
218
327
|
const id = test.metadata?.id;
|
|
@@ -223,14 +332,15 @@ function auditGameplayRun(options, tests) {
|
|
|
223
332
|
}
|
|
224
333
|
}
|
|
225
334
|
const registeredScenes = normalizeSet(
|
|
226
|
-
|
|
335
|
+
observedEvidence.flatMap((evidence) => evidence.registeredScenes)
|
|
227
336
|
);
|
|
228
337
|
const expectedScenes = normalizeSet(options.scenes);
|
|
229
338
|
for (const scene of expectedScenes) {
|
|
230
|
-
if (!registeredScenes.includes(scene))
|
|
339
|
+
if (!registeredScenes.includes(scene)) {
|
|
231
340
|
issues.push(
|
|
232
|
-
`\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`
|
|
233
342
|
);
|
|
343
|
+
}
|
|
234
344
|
}
|
|
235
345
|
for (const scene of registeredScenes) {
|
|
236
346
|
if (!expectedScenes.includes(scene))
|
|
@@ -260,11 +370,96 @@ function toAuditTest(test) {
|
|
|
260
370
|
metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
|
|
261
371
|
};
|
|
262
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
|
+
}
|
|
263
445
|
function printIssues(stage, issues) {
|
|
264
446
|
if (issues.length === 0) return;
|
|
265
447
|
console.error(`
|
|
266
448
|
GAMEPLAY_AUDIT: ${stage} FAILED`);
|
|
267
|
-
|
|
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
|
+
}
|
|
268
463
|
}
|
|
269
464
|
var GameplayAuditReporter = class {
|
|
270
465
|
projectRoot;
|
|
@@ -305,28 +500,37 @@ var GameplayAuditReporter = class {
|
|
|
305
500
|
(testModule) => [...testModule.children.allTests()].map(toAuditTest)
|
|
306
501
|
);
|
|
307
502
|
const result = auditGameplayRun(this.options, tests);
|
|
308
|
-
|
|
309
|
-
|
|
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)]);
|
|
310
512
|
process.exitCode = 1;
|
|
311
513
|
} else {
|
|
312
514
|
console.log(
|
|
313
515
|
`
|
|
314
516
|
GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
|
|
315
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
|
+
);
|
|
316
521
|
}
|
|
317
522
|
}
|
|
318
523
|
/** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
|
|
319
524
|
reportPreflightIssue(issue) {
|
|
320
525
|
if (this.preflightIssues.has(issue)) return;
|
|
321
526
|
this.preflightIssues.add(issue);
|
|
322
|
-
printIssues("PREFLIGHT", [issue]);
|
|
323
527
|
process.exitCode = 1;
|
|
324
528
|
}
|
|
325
529
|
};
|
|
326
530
|
|
|
327
531
|
// src/vitest-config.ts
|
|
328
532
|
function defineGameVitestConfig(options) {
|
|
329
|
-
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
|
|
533
|
+
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
|
|
330
534
|
if (auditIssues.length > 0) {
|
|
331
535
|
throw new Error(
|
|
332
536
|
`\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
|
|
@@ -369,7 +573,9 @@ function defineGameVitestConfig(options) {
|
|
|
369
573
|
...options.additionalSetupFiles ?? []
|
|
370
574
|
],
|
|
371
575
|
reporters: [
|
|
372
|
-
|
|
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",
|
|
373
579
|
new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
|
|
374
580
|
],
|
|
375
581
|
restoreMocks: true,
|
|
@@ -381,7 +587,9 @@ function defineGameVitestConfig(options) {
|
|
|
381
587
|
"src/scenes/**/*.d.ts",
|
|
382
588
|
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
383
589
|
],
|
|
384
|
-
|
|
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"],
|
|
385
593
|
thresholds: {
|
|
386
594
|
perFile: true,
|
|
387
595
|
lines: 1,
|