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
|
@@ -7,6 +7,8 @@ import { resolve } from "path";
|
|
|
7
7
|
import { defineConfig } from "vitest/config";
|
|
8
8
|
|
|
9
9
|
// src/gameplay-audit.ts
|
|
10
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
10
12
|
function normalizeList(value) {
|
|
11
13
|
if (value === void 0) return [];
|
|
12
14
|
const values = typeof value === "string" ? [value] : value;
|
|
@@ -40,20 +42,35 @@ function createGameplayContractMetadata(contract) {
|
|
|
40
42
|
const scenes = normalizeSet(contract.scenes);
|
|
41
43
|
return {
|
|
42
44
|
id: contract.id.trim(),
|
|
45
|
+
kind: contract.kind,
|
|
43
46
|
scenes,
|
|
44
47
|
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
45
48
|
verified: false
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
|
-
function validateGameplayAuditOptions(options) {
|
|
51
|
+
function validateGameplayAuditOptions(options, projectRoot2) {
|
|
49
52
|
const issues = [];
|
|
50
|
-
const scenes =
|
|
53
|
+
const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
|
|
54
|
+
const mode = options.mode ?? "interactive";
|
|
55
|
+
if (mode === "non-interactive") {
|
|
56
|
+
const justification = options.nonInteractiveJustification?.trim() ?? "";
|
|
57
|
+
if (justification.length < 20) {
|
|
58
|
+
issues.push(
|
|
59
|
+
"non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (mode !== "interactive" && mode !== "non-interactive") {
|
|
64
|
+
issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
|
|
65
|
+
}
|
|
51
66
|
if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
52
67
|
if (scenes.length !== options.scenes.length) {
|
|
53
68
|
issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
|
|
54
69
|
}
|
|
55
70
|
const contractIds = /* @__PURE__ */ new Set();
|
|
56
71
|
const coveredScenes = /* @__PURE__ */ new Set();
|
|
72
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
73
|
+
const invalidKindContracts = [];
|
|
57
74
|
for (const contract of options.contracts) {
|
|
58
75
|
const id = contract.id.trim();
|
|
59
76
|
if (!id) {
|
|
@@ -72,6 +89,27 @@ function validateGameplayAuditOptions(options) {
|
|
|
72
89
|
);
|
|
73
90
|
}
|
|
74
91
|
const contractScenes = normalizeSet(contract.scenes);
|
|
92
|
+
if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
|
|
93
|
+
invalidKindContracts.push(id || "<empty>");
|
|
94
|
+
} else {
|
|
95
|
+
const list = kinds.get(contract.kind) ?? [];
|
|
96
|
+
list.push(contract);
|
|
97
|
+
kinds.set(contract.kind, list);
|
|
98
|
+
}
|
|
99
|
+
if (contract.kind === "playthrough") {
|
|
100
|
+
if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
101
|
+
if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
102
|
+
if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
|
|
103
|
+
}
|
|
104
|
+
if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
|
|
105
|
+
issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
|
|
106
|
+
}
|
|
107
|
+
if (contract.kind === "recovery") {
|
|
108
|
+
if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
109
|
+
if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
110
|
+
if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
|
|
111
|
+
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`);
|
|
112
|
+
}
|
|
75
113
|
if (contractScenes.length === 0) {
|
|
76
114
|
issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
|
|
77
115
|
}
|
|
@@ -84,6 +122,11 @@ function validateGameplayAuditOptions(options) {
|
|
|
84
122
|
}
|
|
85
123
|
}
|
|
86
124
|
}
|
|
125
|
+
if (invalidKindContracts.length > 0) {
|
|
126
|
+
issues.push(
|
|
127
|
+
`\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`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
87
130
|
for (const scene of scenes) {
|
|
88
131
|
if (!coveredScenes.has(scene)) {
|
|
89
132
|
issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
|
|
@@ -91,11 +134,71 @@ function validateGameplayAuditOptions(options) {
|
|
|
91
134
|
}
|
|
92
135
|
if (options.contracts.length === 0)
|
|
93
136
|
issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
137
|
+
if (mode === "interactive") {
|
|
138
|
+
const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
|
|
139
|
+
const hasInput = options.contracts.some((contract) => contract.requireInput);
|
|
140
|
+
const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
|
|
141
|
+
if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
|
|
142
|
+
issues.push(
|
|
143
|
+
'\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'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const entryScene = scenes[0];
|
|
147
|
+
for (const scene of scenes.slice(1)) {
|
|
148
|
+
const transitioned = options.contracts.some(
|
|
149
|
+
(contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
|
|
150
|
+
);
|
|
151
|
+
if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
|
|
152
|
+
}
|
|
153
|
+
if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
|
|
154
|
+
issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
|
|
155
|
+
}
|
|
156
|
+
const restartSignal = projectRoot2 ? containsProductionRestart(projectRoot2) : false;
|
|
157
|
+
if (restartSignal && options.flows?.restart !== true) {
|
|
158
|
+
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");
|
|
159
|
+
} else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
|
|
160
|
+
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
161
|
+
} else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
|
|
162
|
+
issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
94
165
|
return issues;
|
|
95
166
|
}
|
|
167
|
+
function containsProductionRestart(projectRoot2) {
|
|
168
|
+
const sceneRoot = join(projectRoot2, "src", "scenes");
|
|
169
|
+
const visit = (directory) => {
|
|
170
|
+
let entries;
|
|
171
|
+
try {
|
|
172
|
+
entries = readdirSync(directory);
|
|
173
|
+
} catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const path = join(directory, entry);
|
|
178
|
+
let stats;
|
|
179
|
+
try {
|
|
180
|
+
stats = statSync(path);
|
|
181
|
+
} catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (stats.isDirectory() && visit(path)) return true;
|
|
185
|
+
if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
|
|
186
|
+
try {
|
|
187
|
+
const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
188
|
+
if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
};
|
|
194
|
+
return visit(sceneRoot);
|
|
195
|
+
}
|
|
96
196
|
function getDefinitionIssues(expected, actual) {
|
|
97
197
|
const issues = [];
|
|
98
198
|
const expectedMetadata = createGameplayContractMetadata(expected);
|
|
199
|
+
if (expectedMetadata.kind !== actual.kind) {
|
|
200
|
+
issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
|
|
201
|
+
}
|
|
99
202
|
if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
|
|
100
203
|
issues.push(
|
|
101
204
|
`contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
@@ -150,12 +253,15 @@ function auditGameplayCollection(options, file, tests) {
|
|
|
150
253
|
function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
151
254
|
const issues = [];
|
|
152
255
|
const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
|
|
153
|
-
if (requirements.requireInput && inputEvents === 0)
|
|
256
|
+
if (requirements.requireInput && inputEvents === 0) {
|
|
154
257
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
|
|
155
|
-
|
|
258
|
+
}
|
|
259
|
+
if (requirements.requireFrameAdvance && evidence.frames === 0) {
|
|
156
260
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
|
|
157
|
-
|
|
261
|
+
}
|
|
262
|
+
if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
|
|
158
263
|
issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
|
|
264
|
+
}
|
|
159
265
|
for (const target of normalizeList(requirements.requireTransition)) {
|
|
160
266
|
if (!evidence.transitions.some((transition) => transition.to === target)) {
|
|
161
267
|
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
|
|
@@ -166,21 +272,24 @@ function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
|
166
272
|
issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
|
|
167
273
|
}
|
|
168
274
|
for (const scene of normalizeList(requirements.requireRestart)) {
|
|
169
|
-
if (!evidence.restartedScenes.includes(scene))
|
|
275
|
+
if (!evidence.restartedScenes.includes(scene)) {
|
|
170
276
|
issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
|
|
277
|
+
}
|
|
171
278
|
}
|
|
172
279
|
for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
|
|
173
|
-
if (!evidence.checkpoints.includes(checkpoint))
|
|
280
|
+
if (!evidence.checkpoints.includes(checkpoint)) {
|
|
174
281
|
issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
|
|
282
|
+
}
|
|
175
283
|
}
|
|
176
|
-
if (requirements.requireDestroy && !evidence.destroyed)
|
|
284
|
+
if (requirements.requireDestroy && !evidence.destroyed) {
|
|
177
285
|
issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
|
|
286
|
+
}
|
|
178
287
|
return issues;
|
|
179
288
|
}
|
|
180
289
|
function auditGameplayRun(options, tests) {
|
|
181
290
|
const issues = [];
|
|
182
291
|
const actualContracts = tests.filter((test) => test.metadata);
|
|
183
|
-
const
|
|
292
|
+
const observedEvidence = [];
|
|
184
293
|
for (const expected of options.contracts) {
|
|
185
294
|
const matches = actualContracts.filter(
|
|
186
295
|
(test2) => test2.metadata?.id === expected.id
|
|
@@ -196,6 +305,7 @@ function auditGameplayRun(options, tests) {
|
|
|
196
305
|
const test = matches[0];
|
|
197
306
|
const metadata = test?.metadata;
|
|
198
307
|
if (!test || !metadata) continue;
|
|
308
|
+
if (metadata.evidence) observedEvidence.push(metadata.evidence);
|
|
199
309
|
if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
|
|
200
310
|
issues.push(
|
|
201
311
|
`contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
|
|
@@ -216,7 +326,6 @@ function auditGameplayRun(options, tests) {
|
|
|
216
326
|
metadata.evidence
|
|
217
327
|
);
|
|
218
328
|
issues.push(...evidenceIssues);
|
|
219
|
-
if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
|
|
220
329
|
}
|
|
221
330
|
for (const test of actualContracts) {
|
|
222
331
|
const id = test.metadata?.id;
|
|
@@ -227,14 +336,15 @@ function auditGameplayRun(options, tests) {
|
|
|
227
336
|
}
|
|
228
337
|
}
|
|
229
338
|
const registeredScenes = normalizeSet(
|
|
230
|
-
|
|
339
|
+
observedEvidence.flatMap((evidence) => evidence.registeredScenes)
|
|
231
340
|
);
|
|
232
341
|
const expectedScenes = normalizeSet(options.scenes);
|
|
233
342
|
for (const scene of expectedScenes) {
|
|
234
|
-
if (!registeredScenes.includes(scene))
|
|
343
|
+
if (!registeredScenes.includes(scene)) {
|
|
235
344
|
issues.push(
|
|
236
|
-
`\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`
|
|
237
346
|
);
|
|
347
|
+
}
|
|
238
348
|
}
|
|
239
349
|
for (const scene of registeredScenes) {
|
|
240
350
|
if (!expectedScenes.includes(scene))
|
|
@@ -264,11 +374,96 @@ function toAuditTest(test) {
|
|
|
264
374
|
metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
|
|
265
375
|
};
|
|
266
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
|
+
}
|
|
267
449
|
function printIssues(stage, issues) {
|
|
268
450
|
if (issues.length === 0) return;
|
|
269
451
|
console.error(`
|
|
270
452
|
GAMEPLAY_AUDIT: ${stage} FAILED`);
|
|
271
|
-
|
|
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
|
+
}
|
|
272
467
|
}
|
|
273
468
|
var GameplayAuditReporter = class {
|
|
274
469
|
projectRoot;
|
|
@@ -309,28 +504,37 @@ var GameplayAuditReporter = class {
|
|
|
309
504
|
(testModule) => [...testModule.children.allTests()].map(toAuditTest)
|
|
310
505
|
);
|
|
311
506
|
const result = auditGameplayRun(this.options, tests);
|
|
312
|
-
|
|
313
|
-
|
|
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)]);
|
|
314
516
|
process.exitCode = 1;
|
|
315
517
|
} else {
|
|
316
518
|
console.log(
|
|
317
519
|
`
|
|
318
520
|
GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
|
|
319
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
|
+
);
|
|
320
525
|
}
|
|
321
526
|
}
|
|
322
527
|
/** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
|
|
323
528
|
reportPreflightIssue(issue) {
|
|
324
529
|
if (this.preflightIssues.has(issue)) return;
|
|
325
530
|
this.preflightIssues.add(issue);
|
|
326
|
-
printIssues("PREFLIGHT", [issue]);
|
|
327
531
|
process.exitCode = 1;
|
|
328
532
|
}
|
|
329
533
|
};
|
|
330
534
|
|
|
331
535
|
// src/vitest-config.ts
|
|
332
536
|
function defineGameVitestConfig(options) {
|
|
333
|
-
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
|
|
537
|
+
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
|
|
334
538
|
if (auditIssues.length > 0) {
|
|
335
539
|
throw new Error(
|
|
336
540
|
`\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
|
|
@@ -373,7 +577,9 @@ function defineGameVitestConfig(options) {
|
|
|
373
577
|
...options.additionalSetupFiles ?? []
|
|
374
578
|
],
|
|
375
579
|
reporters: [
|
|
376
|
-
|
|
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",
|
|
377
583
|
new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
|
|
378
584
|
],
|
|
379
585
|
restoreMocks: true,
|
|
@@ -385,7 +591,9 @@ function defineGameVitestConfig(options) {
|
|
|
385
591
|
"src/scenes/**/*.d.ts",
|
|
386
592
|
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
387
593
|
],
|
|
388
|
-
|
|
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"],
|
|
389
597
|
thresholds: {
|
|
390
598
|
perFile: true,
|
|
391
599
|
lines: 1,
|
|
@@ -400,10 +608,13 @@ function defineGameVitestConfig(options) {
|
|
|
400
608
|
// src/lint/vitest-config.test.ts
|
|
401
609
|
var projectRoot = resolve2(import.meta.dirname, "../..");
|
|
402
610
|
var gameplayAudit = {
|
|
611
|
+
mode: "non-interactive",
|
|
612
|
+
nonInteractiveJustification: "\u56FA\u5B9A\u5939\u5177\u53EA\u9A8C\u8BC1\u5171\u4EAB host \u57FA\u7EBF\uFF0C\u4E0D\u5305\u542B\u73A9\u6CD5\u8F93\u5165\u3002",
|
|
403
613
|
scenes: ["InputScene"],
|
|
404
614
|
contracts: [
|
|
405
615
|
{
|
|
406
616
|
id: "InputScene.boot",
|
|
617
|
+
kind: "boot",
|
|
407
618
|
testFile: "tests/input.test.ts",
|
|
408
619
|
scenes: ["InputScene"],
|
|
409
620
|
requireFrameAdvance: true
|
|
@@ -429,7 +640,7 @@ describe("defineGameVitestConfig", () => {
|
|
|
429
640
|
environment: "jsdom",
|
|
430
641
|
environmentOptions: { jsdom: { url: "http://localhost/" } },
|
|
431
642
|
setupFiles: ["miaoda-game-devkit/vitest-setup"],
|
|
432
|
-
reporters: ["
|
|
643
|
+
reporters: ["minimal", expect.any(Object)],
|
|
433
644
|
restoreMocks: true,
|
|
434
645
|
clearMocks: true,
|
|
435
646
|
coverage: {
|
|
@@ -439,7 +650,7 @@ describe("defineGameVitestConfig", () => {
|
|
|
439
650
|
"src/scenes/**/*.d.ts",
|
|
440
651
|
"src/scenes/**/*.{spec,test}.{ts,tsx}"
|
|
441
652
|
],
|
|
442
|
-
reporter: ["text"],
|
|
653
|
+
reporter: ["text-summary"],
|
|
443
654
|
thresholds: {
|
|
444
655
|
perFile: true,
|
|
445
656
|
lines: 1,
|
|
@@ -476,4 +687,16 @@ describe("defineGameVitestConfig", () => {
|
|
|
476
687
|
})
|
|
477
688
|
).toThrow(/ResultScene.*没有任何 gameplay contract/s);
|
|
478
689
|
});
|
|
690
|
+
it("\u62D2\u7EDD\u4EA4\u4E92\u9879\u76EE\u53EA\u6709 boot smoke test", () => {
|
|
691
|
+
expect(
|
|
692
|
+
() => defineGameVitestConfig({
|
|
693
|
+
projectRoot,
|
|
694
|
+
gameplayAudit: {
|
|
695
|
+
mode: "interactive",
|
|
696
|
+
scenes: ["InputScene"],
|
|
697
|
+
contracts: gameplayAudit.contracts
|
|
698
|
+
}
|
|
699
|
+
})
|
|
700
|
+
).toThrow(/缺少最低玩法基线/);
|
|
701
|
+
});
|
|
479
702
|
});
|
package/dist/vitest-config.d.mts
CHANGED
package/dist/vitest-config.d.ts
CHANGED