miaoda-game-devkit 0.2.11 → 0.2.13
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 +18 -8
- package/bin/miaoda-phaser-game-lint.js +3 -0
- package/bin/miaoda-react-game-lint.js +3 -0
- package/dist/cli/phaser-lint.js +163 -0
- package/dist/cli/react-lint.js +163 -0
- package/dist/lint/setup.mjs +23 -41
- package/dist/react/vitest-config.js +34 -1
- package/dist/react/vitest-config.mjs +35 -2
- package/dist/react/vitest-setup.js +72 -1
- package/dist/react/vitest-setup.mjs +72 -1
- package/dist/vite.js +13 -0
- package/dist/vite.mjs +13 -0
- package/dist/vitest-config.js +16 -1
- package/dist/vitest-config.mjs +16 -1
- package/package.json +7 -4
- package/bin/miaoda-game-lint.js +0 -3
- package/dist/cli/lint.js +0 -127
- package/dist/lint/contracts.config.mjs +0 -38
- package/dist/lint/game-telemetry.contract.mjs +0 -1402
- package/dist/lint/gameplay-audit.contract.mjs +0 -909
- package/dist/lint/gameplay-contract.contract.mjs +0 -1408
- package/dist/lint/phaser-headless.contract.mjs +0 -1856
- package/dist/lint/phaser-text-assertions.contract.mjs +0 -32
- package/dist/lint/resource-import-plugin.contract.mjs +0 -97
- package/dist/lint/vitest-config.contract.mjs +0 -1000
|
@@ -1,909 +0,0 @@
|
|
|
1
|
-
// src/lint/gameplay-audit.test.ts
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
3
|
-
|
|
4
|
-
// src/gameplay-audit.ts
|
|
5
|
-
import { readdirSync, readFileSync, statSync } from "fs";
|
|
6
|
-
import { join } from "path";
|
|
7
|
-
function normalizeList(value) {
|
|
8
|
-
if (value === void 0) return [];
|
|
9
|
-
const values = typeof value === "string" ? [value] : value;
|
|
10
|
-
return [...new Set(values.map((item) => item.trim()).filter(Boolean))];
|
|
11
|
-
}
|
|
12
|
-
function normalizeSet(values) {
|
|
13
|
-
return [
|
|
14
|
-
...new Set(values.map((value) => value.trim()).filter(Boolean))
|
|
15
|
-
].sort();
|
|
16
|
-
}
|
|
17
|
-
function sameSet(left, right) {
|
|
18
|
-
return JSON.stringify(normalizeSet(left)) === JSON.stringify(normalizeSet(right));
|
|
19
|
-
}
|
|
20
|
-
function normalizeGameplayRequirements(requirements, scenes) {
|
|
21
|
-
const requiredScenes = normalizeSet([
|
|
22
|
-
...scenes,
|
|
23
|
-
...normalizeList(requirements.requireScene)
|
|
24
|
-
]);
|
|
25
|
-
return {
|
|
26
|
-
requireInput: requirements.requireInput || void 0,
|
|
27
|
-
requireFrameAdvance: requirements.requireFrameAdvance || void 0,
|
|
28
|
-
requirePhysicsStep: requirements.requirePhysicsStep || void 0,
|
|
29
|
-
requireTransition: normalizeList(requirements.requireTransition),
|
|
30
|
-
requireScene: requiredScenes,
|
|
31
|
-
requireRestart: normalizeList(requirements.requireRestart),
|
|
32
|
-
requireCheckpoint: normalizeList(requirements.requireCheckpoint),
|
|
33
|
-
requireDestroy: true
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function createGameplayContractMetadata(contract) {
|
|
37
|
-
const scenes = normalizeSet(contract.scenes);
|
|
38
|
-
return {
|
|
39
|
-
id: contract.id.trim(),
|
|
40
|
-
kind: contract.kind,
|
|
41
|
-
scenes,
|
|
42
|
-
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
43
|
-
verified: false
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function validateGameplayAuditOptions(options2, projectRoot) {
|
|
47
|
-
const issues = [];
|
|
48
|
-
const contracts = options2.contracts;
|
|
49
|
-
const scenes = [...new Set(options2.scenes.map((scene) => scene.trim()).filter(Boolean))];
|
|
50
|
-
const mode = options2.mode ?? "interactive";
|
|
51
|
-
if (mode === "non-interactive") {
|
|
52
|
-
const justification = options2.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
|
-
}
|
|
62
|
-
if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
63
|
-
if (scenes.length !== options2.scenes.length) {
|
|
64
|
-
issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
|
|
65
|
-
}
|
|
66
|
-
const contractIds = /* @__PURE__ */ new Set();
|
|
67
|
-
const coveredScenes = /* @__PURE__ */ new Set();
|
|
68
|
-
const kinds = /* @__PURE__ */ new Map();
|
|
69
|
-
const invalidKindContracts = [];
|
|
70
|
-
for (const contract of contracts ?? []) {
|
|
71
|
-
const id = contract.id.trim();
|
|
72
|
-
if (!id) {
|
|
73
|
-
issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
|
|
74
|
-
} else if (id !== contract.id) {
|
|
75
|
-
issues.push(`contract id "${contract.id}" \u4E0D\u80FD\u5305\u542B\u9996\u5C3E\u7A7A\u767D\u3002`);
|
|
76
|
-
} else if (contractIds.has(id)) {
|
|
77
|
-
issues.push(`gameplayAudit.contracts \u5305\u542B\u91CD\u590D contract id\uFF1A${id}\u3002`);
|
|
78
|
-
} else {
|
|
79
|
-
contractIds.add(id);
|
|
80
|
-
}
|
|
81
|
-
const testFile = contract.testFile.replaceAll("\\", "/");
|
|
82
|
-
if (!/^tests\/.+\.test\.ts$/.test(testFile) || testFile.split("/").includes("..")) {
|
|
83
|
-
issues.push(
|
|
84
|
-
`contract "${id || "<empty>"}" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
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.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
97
|
-
if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
|
|
98
|
-
}
|
|
99
|
-
if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
|
|
100
|
-
issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
|
|
101
|
-
}
|
|
102
|
-
if (contract.kind === "recovery") {
|
|
103
|
-
if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
104
|
-
if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
|
|
105
|
-
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`);
|
|
106
|
-
}
|
|
107
|
-
if (contractScenes.length === 0) {
|
|
108
|
-
issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
|
|
109
|
-
}
|
|
110
|
-
for (const scene of contractScenes) {
|
|
111
|
-
coveredScenes.add(scene);
|
|
112
|
-
if (!scenes.includes(scene)) {
|
|
113
|
-
issues.push(
|
|
114
|
-
`contract "${id}" \u5F15\u7528\u4E86\u672A\u5217\u5165 gameplayAudit.scenes \u7684 Scene\uFF1A${scene}\u3002`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (invalidKindContracts.length > 0) {
|
|
120
|
-
issues.push(
|
|
121
|
-
`\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`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
if (contracts) {
|
|
125
|
-
for (const scene of scenes) {
|
|
126
|
-
if (!coveredScenes.has(scene)) {
|
|
127
|
-
issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (contracts.length === 0)
|
|
131
|
-
issues.push("gameplayAudit.contracts \u5B58\u5728\u65F6\u4E0D\u80FD\u4E3A\u7A7A\uFF1B\u4E0D\u4F7F\u7528\u663E\u5F0F\u6E05\u5355\u8BF7\u7701\u7565\u8BE5\u5B57\u6BB5\u3002");
|
|
132
|
-
}
|
|
133
|
-
if (mode === "interactive" && contracts) {
|
|
134
|
-
const hasPlaythrough = (kinds.get("playthrough") ?? []).some(
|
|
135
|
-
(contract) => Boolean(contract.requireFrameAdvance) && normalizeList(contract.requireCheckpoint).length > 0
|
|
136
|
-
);
|
|
137
|
-
const hasInput = contracts.some((contract) => contract.requireInput);
|
|
138
|
-
if (!hasPlaythrough || !hasInput) {
|
|
139
|
-
issues.push(
|
|
140
|
-
"\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A playthrough contract \u5FC5\u987B\u58F0\u660E requireFrameAdvance \u548C checkpoint\uFF0C\u4E14\u81F3\u5C11\u4E00\u4E2A contract \u5FC5\u987B\u58F0\u660E requireInput: true\u3002"
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
|
|
144
|
-
if (restartSignal && options2.flows?.restart !== true) {
|
|
145
|
-
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");
|
|
146
|
-
} else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
|
|
147
|
-
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
148
|
-
} else if ((options2.flows?.restart || options2.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
|
|
149
|
-
issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return issues;
|
|
153
|
-
}
|
|
154
|
-
function containsProductionRestart(projectRoot) {
|
|
155
|
-
const sceneRoot = join(projectRoot, "src", "scenes");
|
|
156
|
-
const visit = (directory) => {
|
|
157
|
-
let entries;
|
|
158
|
-
try {
|
|
159
|
-
entries = readdirSync(directory);
|
|
160
|
-
} catch {
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
for (const entry of entries) {
|
|
164
|
-
const path = join(directory, entry);
|
|
165
|
-
let stats;
|
|
166
|
-
try {
|
|
167
|
-
stats = statSync(path);
|
|
168
|
-
} catch {
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
if (stats.isDirectory() && visit(path)) return true;
|
|
172
|
-
if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
|
|
173
|
-
try {
|
|
174
|
-
const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
175
|
-
if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
|
|
176
|
-
} catch {
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return false;
|
|
180
|
-
};
|
|
181
|
-
return visit(sceneRoot);
|
|
182
|
-
}
|
|
183
|
-
function getDefinitionIssues(expected, actual) {
|
|
184
|
-
const issues = [];
|
|
185
|
-
const expectedMetadata = createGameplayContractMetadata(expected);
|
|
186
|
-
if (expectedMetadata.kind !== actual.kind) {
|
|
187
|
-
issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
|
|
188
|
-
}
|
|
189
|
-
if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
|
|
190
|
-
issues.push(
|
|
191
|
-
`contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
if (JSON.stringify(expectedMetadata.requirements) !== JSON.stringify(actual.requirements)) {
|
|
195
|
-
issues.push(
|
|
196
|
-
`contract "${expected.id}" \u7684 evidence requirements \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
return issues;
|
|
200
|
-
}
|
|
201
|
-
function auditGameplayCollection(options2, file, tests) {
|
|
202
|
-
const normalizedFile = file.replaceAll("\\", "/");
|
|
203
|
-
const expected = (options2.contracts ?? []).filter(
|
|
204
|
-
(contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
|
|
205
|
-
);
|
|
206
|
-
const actual = tests.filter((test) => test.metadata);
|
|
207
|
-
const issues = [];
|
|
208
|
-
if (!options2.contracts) {
|
|
209
|
-
const actualIds = /* @__PURE__ */ new Set();
|
|
210
|
-
for (const test of actual) {
|
|
211
|
-
const id = test.metadata?.id;
|
|
212
|
-
if (!id) continue;
|
|
213
|
-
if (actualIds.has(id)) issues.push(`contract "${id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`);
|
|
214
|
-
actualIds.add(id);
|
|
215
|
-
}
|
|
216
|
-
return { passed: issues.length === 0, issues };
|
|
217
|
-
}
|
|
218
|
-
for (const contract of expected) {
|
|
219
|
-
const matches = actual.filter((test) => test.metadata?.id === contract.id);
|
|
220
|
-
if (matches.length === 0) {
|
|
221
|
-
issues.push(
|
|
222
|
-
`\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "${contract.id}" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08${normalizedFile}\uFF09\u3002`
|
|
223
|
-
);
|
|
224
|
-
} else if (matches.length > 1) {
|
|
225
|
-
issues.push(
|
|
226
|
-
`contract "${contract.id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`
|
|
227
|
-
);
|
|
228
|
-
} else if (matches[0]?.metadata) {
|
|
229
|
-
issues.push(...getDefinitionIssues(contract, matches[0].metadata));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
for (const test of actual) {
|
|
233
|
-
const id = test.metadata?.id;
|
|
234
|
-
if (!id) continue;
|
|
235
|
-
const expectedContract = options2.contracts.find(
|
|
236
|
-
(contract) => contract.id === id
|
|
237
|
-
);
|
|
238
|
-
if (!expectedContract) {
|
|
239
|
-
issues.push(
|
|
240
|
-
`\u6D4B\u8BD5 "${test.name}" \u58F0\u660E\u4E86\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\u3002`
|
|
241
|
-
);
|
|
242
|
-
} else if (expectedContract.testFile.replaceAll("\\", "/") !== normalizedFile) {
|
|
243
|
-
issues.push(
|
|
244
|
-
`contract "${id}" \u5FC5\u987B\u4F4D\u4E8E ${expectedContract.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${normalizedFile}\u3002`
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return { passed: issues.length === 0, issues };
|
|
249
|
-
}
|
|
250
|
-
function getGameplayEvidenceIssues(id, requirements, evidence) {
|
|
251
|
-
const issues = [];
|
|
252
|
-
const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
|
|
253
|
-
if (requirements.requireInput && inputEvents === 0) {
|
|
254
|
-
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
|
|
255
|
-
}
|
|
256
|
-
if (requirements.requireFrameAdvance && evidence.frames === 0) {
|
|
257
|
-
issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
|
|
258
|
-
}
|
|
259
|
-
if (requirements.requirePhysicsStep && evidence.physicsSteps === 0) {
|
|
260
|
-
issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
|
|
261
|
-
}
|
|
262
|
-
for (const target of normalizeList(requirements.requireTransition)) {
|
|
263
|
-
if (!evidence.transitions.some((transition) => transition.to === target)) {
|
|
264
|
-
issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
for (const scene of normalizeList(requirements.requireScene)) {
|
|
268
|
-
if (!evidence.visitedScenes.includes(scene))
|
|
269
|
-
issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
|
|
270
|
-
}
|
|
271
|
-
for (const scene of normalizeList(requirements.requireRestart)) {
|
|
272
|
-
if (!evidence.restartedScenes.includes(scene)) {
|
|
273
|
-
issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
|
|
277
|
-
if (!evidence.checkpoints.includes(checkpoint)) {
|
|
278
|
-
issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
if (requirements.requireDestroy && !evidence.destroyed) {
|
|
282
|
-
issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
|
|
283
|
-
}
|
|
284
|
-
return issues;
|
|
285
|
-
}
|
|
286
|
-
function auditGameplayRun(options2, tests) {
|
|
287
|
-
const issues = [];
|
|
288
|
-
const actualContracts = tests.filter((test) => test.metadata);
|
|
289
|
-
const observedEvidence = [];
|
|
290
|
-
const discoveredContracts = actualContracts.flatMap((test) => {
|
|
291
|
-
const metadata = test.metadata;
|
|
292
|
-
if (!metadata) return [];
|
|
293
|
-
return [{
|
|
294
|
-
id: metadata.id,
|
|
295
|
-
kind: metadata.kind,
|
|
296
|
-
testFile: test.file,
|
|
297
|
-
scenes: metadata.scenes,
|
|
298
|
-
...metadata.requirements
|
|
299
|
-
}];
|
|
300
|
-
});
|
|
301
|
-
const expectedContracts = options2.contracts ?? discoveredContracts;
|
|
302
|
-
if (!options2.contracts) {
|
|
303
|
-
const discoveredIssues = validateGameplayAuditOptions(
|
|
304
|
-
{ ...options2, contracts: discoveredContracts }
|
|
305
|
-
);
|
|
306
|
-
issues.push(...discoveredIssues);
|
|
307
|
-
}
|
|
308
|
-
for (const expected of expectedContracts) {
|
|
309
|
-
const matches = actualContracts.filter(
|
|
310
|
-
(test2) => test2.metadata?.id === expected.id
|
|
311
|
-
);
|
|
312
|
-
if (matches.length === 0) {
|
|
313
|
-
issues.push(`\u7F3A\u5C11 gameplay contract "${expected.id}"\u3002`);
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if (matches.length > 1) {
|
|
317
|
-
issues.push(`gameplay contract "${expected.id}" \u88AB\u591A\u4E2A\u6D4B\u8BD5\u91CD\u590D\u58F0\u660E\u3002`);
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
const test = matches[0];
|
|
321
|
-
const metadata = test?.metadata;
|
|
322
|
-
if (!test || !metadata) continue;
|
|
323
|
-
if (metadata.evidence) observedEvidence.push(metadata.evidence);
|
|
324
|
-
if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
|
|
325
|
-
issues.push(
|
|
326
|
-
`contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
issues.push(...getDefinitionIssues(expected, metadata));
|
|
330
|
-
if (test.state !== "passed") {
|
|
331
|
-
if (test.error) {
|
|
332
|
-
issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
|
|
333
|
-
} else {
|
|
334
|
-
issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
|
|
335
|
-
}
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (!metadata.verified || !metadata.evidence) {
|
|
339
|
-
issues.push(`contract "${expected.id}" \u672A\u5B8C\u6210 onTestFinished \u8BC1\u636E\u6821\u9A8C\u3002`);
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
const evidenceIssues = getGameplayEvidenceIssues(
|
|
343
|
-
expected.id,
|
|
344
|
-
metadata.requirements,
|
|
345
|
-
metadata.evidence
|
|
346
|
-
);
|
|
347
|
-
issues.push(...evidenceIssues);
|
|
348
|
-
}
|
|
349
|
-
for (const test of actualContracts) {
|
|
350
|
-
const id = test.metadata?.id;
|
|
351
|
-
if (id && options2.contracts && !options2.contracts.some((contract) => contract.id === id)) {
|
|
352
|
-
issues.push(
|
|
353
|
-
`\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
const registeredScenes = normalizeSet(
|
|
358
|
-
observedEvidence.flatMap((evidence) => evidence.registeredScenes)
|
|
359
|
-
);
|
|
360
|
-
const expectedScenes = normalizeSet(options2.scenes);
|
|
361
|
-
if (observedEvidence.length > 0) {
|
|
362
|
-
for (const scene of expectedScenes) {
|
|
363
|
-
if (!registeredScenes.includes(scene)) {
|
|
364
|
-
issues.push(
|
|
365
|
-
`\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
for (const scene of registeredScenes) {
|
|
370
|
-
if (!expectedScenes.includes(scene)) {
|
|
371
|
-
issues.push(
|
|
372
|
-
`host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return { passed: issues.length === 0, issues: [...new Set(issues)] };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// src/gameplay-audit-reporter.ts
|
|
381
|
-
import { relative } from "path";
|
|
382
|
-
function formatGameplayIssues(issues) {
|
|
383
|
-
const groups = /* @__PURE__ */ new Map();
|
|
384
|
-
const repairKeys = /* @__PURE__ */ new Set();
|
|
385
|
-
const repairs = [];
|
|
386
|
-
const missingByFile = /* @__PURE__ */ new Map();
|
|
387
|
-
const missingIds = /* @__PURE__ */ new Set();
|
|
388
|
-
for (const issue of issues) {
|
|
389
|
-
const match = issue.match(
|
|
390
|
-
/没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
|
|
391
|
-
);
|
|
392
|
-
if (!match) continue;
|
|
393
|
-
const [, id, file] = match;
|
|
394
|
-
missingIds.add(id);
|
|
395
|
-
const ids = missingByFile.get(file) ?? [];
|
|
396
|
-
if (!ids.includes(id)) ids.push(id);
|
|
397
|
-
missingByFile.set(file, ids);
|
|
398
|
-
}
|
|
399
|
-
const repairText = {
|
|
400
|
-
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",
|
|
401
|
-
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",
|
|
402
|
-
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",
|
|
403
|
-
transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
|
|
404
|
-
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",
|
|
405
|
-
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",
|
|
406
|
-
checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
|
|
407
|
-
destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
|
|
408
|
-
registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
|
|
409
|
-
missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
|
|
410
|
-
test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002",
|
|
411
|
-
unhandled: "\u5F02\u6B65\u9519\u8BEF\uFF1A\u5148\u4FEE\u590D\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u9519\u8BEF\uFF1B\u8F93\u5165\u3001\u5E27\u63A8\u8FDB\u548C\u751F\u547D\u5468\u671F\u64CD\u4F5C\u90FD\u8981\u5728\u6B63\u786E\u7684 host \u751F\u547D\u5468\u671F\u5185\u5E76\u4F7F\u7528 await\u3002"
|
|
412
|
-
};
|
|
413
|
-
const getCategory = (issue) => {
|
|
414
|
-
if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
|
|
415
|
-
if (issue.includes("\u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27")) return { category: "frames", key: "frames" };
|
|
416
|
-
if (issue.includes("\u672A\u63A8\u8FDB Arcade Physics")) return { category: "physics", key: "physics" };
|
|
417
|
-
if (issue.includes("\u672A\u8BB0\u5F55\u5230 ") && issue.includes("Scene \u8F6C\u573A")) {
|
|
418
|
-
const target = issue.match(/未记录到 (.+?) 的 Scene 转场/)?.[1] ?? "unknown";
|
|
419
|
-
return { category: "transition", key: `transition:${target}` };
|
|
420
|
-
}
|
|
421
|
-
if (issue.includes("\u672A\u8BBF\u95EE Scene")) {
|
|
422
|
-
const scene = issue.match(/未访问 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
423
|
-
return { category: "visit", key: `visit:${scene}` };
|
|
424
|
-
}
|
|
425
|
-
if (issue.includes("\u672A\u91CD\u542F Scene")) {
|
|
426
|
-
const scene = issue.match(/未重启 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
427
|
-
return { category: "restart", key: `restart:${scene}` };
|
|
428
|
-
}
|
|
429
|
-
if (issue.includes("\u7F3A\u5C11 checkpoint")) return { category: "checkpoint", key: `checkpoint:${issue.match(/checkpoint "([^"]+)"/)?.[1] ?? issue}` };
|
|
430
|
-
if (issue.includes("\u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1")) return { category: "destroy", key: "destroy" };
|
|
431
|
-
if (issue.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
|
|
432
|
-
const scene = issue.match(/生产 Scene "([^"]+)"/)?.[1] ?? "unknown";
|
|
433
|
-
return { category: "registry", key: `registry:${scene}` };
|
|
434
|
-
}
|
|
435
|
-
if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
|
|
436
|
-
if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
|
|
437
|
-
const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
|
|
438
|
-
return { category: "test", key: `test:${message}` };
|
|
439
|
-
}
|
|
440
|
-
if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
|
|
441
|
-
const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
|
|
442
|
-
return { category: "unhandled", key: `test:${message}` };
|
|
443
|
-
}
|
|
444
|
-
if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
|
|
445
|
-
return { category: "other", key: `other:${issue}` };
|
|
446
|
-
};
|
|
447
|
-
for (const [file, ids] of missingByFile) {
|
|
448
|
-
groups.set(`missing:${file}`, {
|
|
449
|
-
text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
|
|
450
|
-
ids: [],
|
|
451
|
-
key: `missing:${file}`
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
if (missingByFile.size > 0) {
|
|
455
|
-
repairKeys.add("missing");
|
|
456
|
-
repairs.push(repairText.missing);
|
|
457
|
-
}
|
|
458
|
-
for (const rawIssue of [...new Set(issues)]) {
|
|
459
|
-
const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
|
|
460
|
-
if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
|
|
461
|
-
const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
|
|
462
|
-
if (missingContract && missingIds.has(missingContract)) continue;
|
|
463
|
-
if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
|
|
467
|
-
const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
|
|
468
|
-
const { category, key } = getCategory(compact);
|
|
469
|
-
const group = groups.get(key);
|
|
470
|
-
if (group) {
|
|
471
|
-
if (id && !group.ids.includes(id)) group.ids.push(id);
|
|
472
|
-
} else {
|
|
473
|
-
groups.set(key, { text: readable, ids: id ? [id] : [], key });
|
|
474
|
-
}
|
|
475
|
-
if (repairText[category] && !repairKeys.has(category)) {
|
|
476
|
-
repairKeys.add(category);
|
|
477
|
-
repairs.push(repairText[category]);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
const grouped = [...groups.values()];
|
|
481
|
-
const visible = grouped.slice(0, 8).map((group) => {
|
|
482
|
-
if (group.ids.length <= 1) return group.text;
|
|
483
|
-
return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
|
|
484
|
-
});
|
|
485
|
-
return {
|
|
486
|
-
issues: visible.map(
|
|
487
|
-
(issue) => issue.length > 360 ? `${issue.slice(0, 357)}...` : issue
|
|
488
|
-
),
|
|
489
|
-
repairs: repairs.slice(0, 8),
|
|
490
|
-
omitted: Math.max(0, grouped.length - visible.length)
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// src/lint/gameplay-audit.test.ts
|
|
495
|
-
var options = {
|
|
496
|
-
mode: "interactive",
|
|
497
|
-
scenes: ["IndexScene", "BattleScene"],
|
|
498
|
-
contracts: [
|
|
499
|
-
{
|
|
500
|
-
id: "gameplay.main-flow",
|
|
501
|
-
kind: "playthrough",
|
|
502
|
-
testFile: "tests/gameplay.test.ts",
|
|
503
|
-
scenes: ["IndexScene", "BattleScene"],
|
|
504
|
-
requireInput: true,
|
|
505
|
-
requireFrameAdvance: true,
|
|
506
|
-
requireTransition: "BattleScene",
|
|
507
|
-
requireCheckpoint: ["progress", "recovery"],
|
|
508
|
-
requireRestart: "BattleScene"
|
|
509
|
-
}
|
|
510
|
-
]
|
|
511
|
-
};
|
|
512
|
-
function createEvidence() {
|
|
513
|
-
return {
|
|
514
|
-
frames: 3,
|
|
515
|
-
physicsSteps: 0,
|
|
516
|
-
mouseEvents: 3,
|
|
517
|
-
keyboardEvents: 0,
|
|
518
|
-
touchEvents: 0,
|
|
519
|
-
clicks: 1,
|
|
520
|
-
registeredScenes: ["IndexScene", "BattleScene"],
|
|
521
|
-
visitedScenes: ["IndexScene", "BattleScene"],
|
|
522
|
-
restartedScenes: ["BattleScene"],
|
|
523
|
-
checkpoints: ["progress", "recovery"],
|
|
524
|
-
destroyed: true,
|
|
525
|
-
transitions: [{ from: "IndexScene", to: "BattleScene", method: "start" }]
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
function createPassingTest() {
|
|
529
|
-
const metadata = createGameplayContractMetadata(options.contracts[0]);
|
|
530
|
-
metadata.evidence = createEvidence();
|
|
531
|
-
metadata.verified = true;
|
|
532
|
-
return {
|
|
533
|
-
file: "tests/gameplay.test.ts",
|
|
534
|
-
name: "main gameplay flow",
|
|
535
|
-
state: "passed",
|
|
536
|
-
metadata
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
describe("gameplay audit", () => {
|
|
540
|
-
it("\u62D2\u7EDD\u4EA4\u4E92\u9879\u76EE\u53EA\u6709 boot contract", () => {
|
|
541
|
-
const result = validateGameplayAuditOptions({
|
|
542
|
-
mode: "interactive",
|
|
543
|
-
scenes: ["IndexScene"],
|
|
544
|
-
contracts: [
|
|
545
|
-
{
|
|
546
|
-
id: "IndexScene.boot",
|
|
547
|
-
kind: "boot",
|
|
548
|
-
testFile: "tests/gameplay.test.ts",
|
|
549
|
-
scenes: ["IndexScene"]
|
|
550
|
-
}
|
|
551
|
-
]
|
|
552
|
-
});
|
|
553
|
-
expect(result).toContain(
|
|
554
|
-
"\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A playthrough contract \u5FC5\u987B\u58F0\u660E requireFrameAdvance \u548C checkpoint\uFF0C\u4E14\u81F3\u5C11\u4E00\u4E2A contract \u5FC5\u987B\u58F0\u660E requireInput: true\u3002"
|
|
555
|
-
);
|
|
556
|
-
});
|
|
557
|
-
it("\u5141\u8BB8\u5E26\u7406\u7531\u7684\u975E\u4EA4\u4E92 boot-only \u9879\u76EE", () => {
|
|
558
|
-
expect(
|
|
559
|
-
validateGameplayAuditOptions({
|
|
560
|
-
mode: "non-interactive",
|
|
561
|
-
nonInteractiveJustification: "\u8FD9\u662F\u53EA\u5C55\u793A\u9759\u6001\u6B22\u8FCE\u753B\u9762\u7684\u6A21\u677F\u5939\u5177\uFF0C\u4E0D\u5305\u542B\u73A9\u5BB6\u8F93\u5165\u3002",
|
|
562
|
-
scenes: ["IndexScene"],
|
|
563
|
-
contracts: [
|
|
564
|
-
{
|
|
565
|
-
id: "IndexScene.boot",
|
|
566
|
-
kind: "boot",
|
|
567
|
-
testFile: "tests/gameplay.test.ts",
|
|
568
|
-
scenes: ["IndexScene"]
|
|
569
|
-
}
|
|
570
|
-
]
|
|
571
|
-
})
|
|
572
|
-
).toEqual([]);
|
|
573
|
-
});
|
|
574
|
-
it("\u5141\u8BB8 Telemetry \u957F\u6D41\u7A0B\u4E0E\u72EC\u7ACB\u771F\u5B9E\u8F93\u5165 contract \u5206\u5DE5", () => {
|
|
575
|
-
expect(
|
|
576
|
-
validateGameplayAuditOptions({
|
|
577
|
-
mode: "interactive",
|
|
578
|
-
scenes: ["IndexScene"],
|
|
579
|
-
contracts: [
|
|
580
|
-
{
|
|
581
|
-
id: "IndexScene.telemetry-playthrough",
|
|
582
|
-
kind: "playthrough",
|
|
583
|
-
testFile: "tests/gameplay.test.ts",
|
|
584
|
-
scenes: ["IndexScene"],
|
|
585
|
-
requireFrameAdvance: true,
|
|
586
|
-
requireCheckpoint: "progress"
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
id: "IndexScene.input-smoke",
|
|
590
|
-
kind: "playthrough",
|
|
591
|
-
testFile: "tests/gameplay.test.ts",
|
|
592
|
-
scenes: ["IndexScene"],
|
|
593
|
-
requireInput: true,
|
|
594
|
-
requireFrameAdvance: true,
|
|
595
|
-
requireCheckpoint: "input-wired"
|
|
596
|
-
}
|
|
597
|
-
]
|
|
598
|
-
})
|
|
599
|
-
).toEqual([]);
|
|
600
|
-
});
|
|
601
|
-
it("\u5141\u8BB8\u7701\u7565\u91CD\u590D inventory \u5E76\u4ECE gameplayTest metadata \u5BA1\u8BA1", () => {
|
|
602
|
-
const inventoryFree = {
|
|
603
|
-
mode: "interactive",
|
|
604
|
-
scenes: ["IndexScene", "BattleScene"]
|
|
605
|
-
};
|
|
606
|
-
expect(validateGameplayAuditOptions(inventoryFree)).toEqual([]);
|
|
607
|
-
expect(auditGameplayRun(inventoryFree, [createPassingTest()])).toEqual({
|
|
608
|
-
passed: true,
|
|
609
|
-
issues: []
|
|
610
|
-
});
|
|
611
|
-
});
|
|
612
|
-
it("\u5F3A\u5236 recovery contract \u540C\u65F6\u58F0\u660E restart \u548C\u4E24\u4E2A checkpoint", () => {
|
|
613
|
-
const result = validateGameplayAuditOptions({
|
|
614
|
-
mode: "non-interactive",
|
|
615
|
-
nonInteractiveJustification: "\u6D4B\u8BD5 recovery contract \u7684\u914D\u7F6E\u6821\u9A8C\uFF0C\u4E0D\u4EE3\u8868\u771F\u5B9E\u4EA7\u54C1\u8C41\u514D\u3002",
|
|
616
|
-
scenes: ["BattleScene"],
|
|
617
|
-
contracts: [{
|
|
618
|
-
id: "BattleScene.recovery",
|
|
619
|
-
kind: "recovery",
|
|
620
|
-
testFile: "tests/gameplay.test.ts",
|
|
621
|
-
scenes: ["BattleScene"],
|
|
622
|
-
requireInput: true,
|
|
623
|
-
requireFrameAdvance: true
|
|
624
|
-
}]
|
|
625
|
-
});
|
|
626
|
-
expect(result).toEqual(expect.arrayContaining([
|
|
627
|
-
'recovery contract "BattleScene.recovery" \u5FC5\u987B\u58F0\u660E requireRestart\u3002',
|
|
628
|
-
'recovery contract "BattleScene.recovery" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002'
|
|
629
|
-
]));
|
|
630
|
-
});
|
|
631
|
-
it("\u5141\u8BB8\u72EC\u7ACB\u8986\u76D6 Scene\uFF0C\u800C\u4E0D\u5F3A\u5236\u5236\u9020\u751F\u4EA7\u8F6C\u573A", () => {
|
|
632
|
-
const result = validateGameplayAuditOptions({
|
|
633
|
-
mode: "interactive",
|
|
634
|
-
scenes: ["MenuScene", "BattleScene"],
|
|
635
|
-
contracts: [
|
|
636
|
-
{
|
|
637
|
-
id: "MenuScene.playthrough",
|
|
638
|
-
kind: "playthrough",
|
|
639
|
-
testFile: "tests/gameplay.test.ts",
|
|
640
|
-
scenes: ["MenuScene"],
|
|
641
|
-
requireInput: true,
|
|
642
|
-
requireFrameAdvance: true,
|
|
643
|
-
requireCheckpoint: "progress"
|
|
644
|
-
},
|
|
645
|
-
{
|
|
646
|
-
id: "BattleScene.boot",
|
|
647
|
-
kind: "boot",
|
|
648
|
-
testFile: "tests/gameplay.test.ts",
|
|
649
|
-
scenes: ["BattleScene"]
|
|
650
|
-
}
|
|
651
|
-
]
|
|
652
|
-
});
|
|
653
|
-
expect(result).toEqual([]);
|
|
654
|
-
});
|
|
655
|
-
it("\u4ECD\u8981\u6C42 transition contract \u58F0\u660E\u76EE\u6807", () => {
|
|
656
|
-
const result = validateGameplayAuditOptions({
|
|
657
|
-
mode: "interactive",
|
|
658
|
-
scenes: ["MenuScene"],
|
|
659
|
-
contracts: [
|
|
660
|
-
{
|
|
661
|
-
id: "MenuScene.playthrough",
|
|
662
|
-
kind: "playthrough",
|
|
663
|
-
testFile: "tests/gameplay.test.ts",
|
|
664
|
-
scenes: ["MenuScene"],
|
|
665
|
-
requireInput: true,
|
|
666
|
-
requireFrameAdvance: true,
|
|
667
|
-
requireCheckpoint: "progress"
|
|
668
|
-
},
|
|
669
|
-
{
|
|
670
|
-
id: "MenuScene.transition",
|
|
671
|
-
kind: "transition",
|
|
672
|
-
testFile: "tests/gameplay.test.ts",
|
|
673
|
-
scenes: ["MenuScene"]
|
|
674
|
-
}
|
|
675
|
-
]
|
|
676
|
-
});
|
|
677
|
-
expect(result).toContain(
|
|
678
|
-
'transition contract "MenuScene.transition" \u5FC5\u987B\u58F0\u660E requireTransition\u3002'
|
|
679
|
-
);
|
|
680
|
-
});
|
|
681
|
-
it("\u58F0\u660E restart flow \u540E\u4ECD\u5F3A\u5236 recovery contract", () => {
|
|
682
|
-
const result = validateGameplayAuditOptions({
|
|
683
|
-
mode: "interactive",
|
|
684
|
-
flows: { restart: true },
|
|
685
|
-
scenes: ["IndexScene"],
|
|
686
|
-
contracts: [
|
|
687
|
-
{
|
|
688
|
-
id: "IndexScene.playthrough",
|
|
689
|
-
kind: "playthrough",
|
|
690
|
-
testFile: "tests/gameplay.test.ts",
|
|
691
|
-
scenes: ["IndexScene"],
|
|
692
|
-
requireInput: true,
|
|
693
|
-
requireFrameAdvance: true,
|
|
694
|
-
requireCheckpoint: "progress"
|
|
695
|
-
}
|
|
696
|
-
]
|
|
697
|
-
});
|
|
698
|
-
expect(result).toContain(
|
|
699
|
-
"\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002"
|
|
700
|
-
);
|
|
701
|
-
});
|
|
702
|
-
it("\u62D2\u7EDD\u672A\u88AB contract \u8986\u76D6\u7684\u751F\u4EA7 Scene", () => {
|
|
703
|
-
expect(
|
|
704
|
-
validateGameplayAuditOptions({
|
|
705
|
-
scenes: ["IndexScene", "ResultScene"],
|
|
706
|
-
contracts: [options.contracts[0]]
|
|
707
|
-
})
|
|
708
|
-
).toContain('Scene "ResultScene" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002');
|
|
709
|
-
});
|
|
710
|
-
it("\u62D2\u7EDD\u4F1A\u5728 metadata \u4E2D\u88AB\u9759\u9ED8\u6539\u5199\u7684 contract id", () => {
|
|
711
|
-
expect(
|
|
712
|
-
validateGameplayAuditOptions({
|
|
713
|
-
scenes: ["IndexScene"],
|
|
714
|
-
contracts: [
|
|
715
|
-
{
|
|
716
|
-
id: " IndexScene.boot ",
|
|
717
|
-
kind: "boot",
|
|
718
|
-
testFile: "tests/gameplay.test.ts",
|
|
719
|
-
scenes: ["IndexScene"]
|
|
720
|
-
}
|
|
721
|
-
]
|
|
722
|
-
})
|
|
723
|
-
).toContain('contract id " IndexScene.boot " \u4E0D\u80FD\u5305\u542B\u9996\u5C3E\u7A7A\u767D\u3002');
|
|
724
|
-
});
|
|
725
|
-
it("\u8BA9 contract testFile \u4E0E\u5171\u4EAB Vitest \u6D4B\u8BD5\u8303\u56F4\u4FDD\u6301\u4E00\u81F4", () => {
|
|
726
|
-
const createOptions = (testFile) => ({
|
|
727
|
-
scenes: ["IndexScene"],
|
|
728
|
-
contracts: [
|
|
729
|
-
{
|
|
730
|
-
id: "IndexScene.boot",
|
|
731
|
-
kind: "boot",
|
|
732
|
-
testFile,
|
|
733
|
-
scenes: ["IndexScene"]
|
|
734
|
-
}
|
|
735
|
-
]
|
|
736
|
-
});
|
|
737
|
-
expect(
|
|
738
|
-
validateGameplayAuditOptions(createOptions("tests/gameplay.test.tsx"))
|
|
739
|
-
).toContain(
|
|
740
|
-
'contract "IndexScene.boot" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002'
|
|
741
|
-
);
|
|
742
|
-
expect(
|
|
743
|
-
validateGameplayAuditOptions(
|
|
744
|
-
createOptions("tests/flows/flow..recovery.test.ts")
|
|
745
|
-
)
|
|
746
|
-
).not.toContain(
|
|
747
|
-
'contract "IndexScene.boot" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002'
|
|
748
|
-
);
|
|
749
|
-
expect(
|
|
750
|
-
validateGameplayAuditOptions(createOptions("tests/../outside.test.ts"))
|
|
751
|
-
).toContain(
|
|
752
|
-
'contract "IndexScene.boot" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002'
|
|
753
|
-
);
|
|
754
|
-
});
|
|
755
|
-
it("\u5728\u6D4B\u8BD5\u6267\u884C\u524D\u62A5\u544A\u672A\u6536\u96C6\u5230\u7684\u9759\u6001 contract", () => {
|
|
756
|
-
const result = auditGameplayCollection(
|
|
757
|
-
options,
|
|
758
|
-
"tests/gameplay.test.ts",
|
|
759
|
-
[]
|
|
760
|
-
);
|
|
761
|
-
expect(result).toEqual({
|
|
762
|
-
passed: false,
|
|
763
|
-
issues: [
|
|
764
|
-
'\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "gameplay.main-flow" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08tests/gameplay.test.ts\uFF09\u3002'
|
|
765
|
-
]
|
|
766
|
-
});
|
|
767
|
-
});
|
|
768
|
-
it("\u5728\u6D4B\u8BD5\u6267\u884C\u524D\u62A5\u544A\u5199\u5728\u9519\u8BEF\u6587\u4EF6\u4E2D\u7684 contract", () => {
|
|
769
|
-
const metadata = createGameplayContractMetadata(options.contracts[0]);
|
|
770
|
-
const result = auditGameplayCollection(options, "tests/wrong.test.ts", [
|
|
771
|
-
{
|
|
772
|
-
file: "tests/wrong.test.ts",
|
|
773
|
-
name: "misplaced gameplay flow",
|
|
774
|
-
state: "pending",
|
|
775
|
-
metadata
|
|
776
|
-
}
|
|
777
|
-
]);
|
|
778
|
-
expect(result).toEqual({
|
|
779
|
-
passed: false,
|
|
780
|
-
issues: [
|
|
781
|
-
'contract "gameplay.main-flow" \u5FC5\u987B\u4F4D\u4E8E tests/gameplay.test.ts\uFF0C\u5B9E\u9645\u4F4D\u4E8E tests/wrong.test.ts\u3002'
|
|
782
|
-
]
|
|
783
|
-
});
|
|
784
|
-
});
|
|
785
|
-
it("\u63A5\u53D7\u5B9A\u4E49\u3001Ledger \u548C Scene registry \u90FD\u5B8C\u6574\u7684\u6D4B\u8BD5\u8FD0\u884C", () => {
|
|
786
|
-
expect(auditGameplayRun(options, [createPassingTest()])).toEqual({
|
|
787
|
-
passed: true,
|
|
788
|
-
issues: []
|
|
789
|
-
});
|
|
790
|
-
});
|
|
791
|
-
it("\u6C47\u603B checkpoint\u3001restart\u3001destroy \u548C\u989D\u5916 Scene \u7F3A\u53E3", () => {
|
|
792
|
-
const evidence = createEvidence();
|
|
793
|
-
evidence.checkpoints = ["progress"];
|
|
794
|
-
evidence.restartedScenes = [];
|
|
795
|
-
evidence.destroyed = false;
|
|
796
|
-
evidence.registeredScenes.push("DebugScene");
|
|
797
|
-
const test = createPassingTest();
|
|
798
|
-
if (!test.metadata) throw new Error("expected gameplay metadata");
|
|
799
|
-
test.metadata.evidence = evidence;
|
|
800
|
-
const result = auditGameplayRun(options, [test]);
|
|
801
|
-
expect(result.passed).toBe(false);
|
|
802
|
-
expect(result.issues).toEqual(
|
|
803
|
-
expect.arrayContaining([
|
|
804
|
-
'contract "gameplay.main-flow" \u672A\u91CD\u542F Scene "BattleScene"\u3002',
|
|
805
|
-
'contract "gameplay.main-flow" \u7F3A\u5C11 checkpoint "recovery"\u3002',
|
|
806
|
-
'contract "gameplay.main-flow" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002'
|
|
807
|
-
])
|
|
808
|
-
);
|
|
809
|
-
});
|
|
810
|
-
it("\u4FDD\u7559\u5931\u8D25 host \u5DF2\u6CE8\u518C\u7684 Scene\uFF0C\u907F\u514D checkpoint \u7F3A\u53E3\u7EA7\u8054\u6210\u6CE8\u518C\u9519\u8BEF", () => {
|
|
811
|
-
const test = createPassingTest();
|
|
812
|
-
if (!test.metadata?.evidence) throw new Error("expected gameplay evidence");
|
|
813
|
-
test.metadata.evidence.checkpoints = ["progress"];
|
|
814
|
-
const result = auditGameplayRun(options, [test]);
|
|
815
|
-
expect(result.issues).toContain(
|
|
816
|
-
'contract "gameplay.main-flow" \u7F3A\u5C11 checkpoint "recovery"\u3002'
|
|
817
|
-
);
|
|
818
|
-
expect(result.issues).not.toContain(
|
|
819
|
-
'\u751F\u4EA7 Scene "IndexScene" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002'
|
|
820
|
-
);
|
|
821
|
-
expect(result.issues).not.toContain(
|
|
822
|
-
'\u751F\u4EA7 Scene "BattleScene" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002'
|
|
823
|
-
);
|
|
824
|
-
});
|
|
825
|
-
it("\u62D2\u7EDD\u901A\u8FC7\u5951\u7EA6\u7684 host registry \u4E2D\u51FA\u73B0\u6E05\u5355\u5916 Scene", () => {
|
|
826
|
-
const test = createPassingTest();
|
|
827
|
-
if (!test.metadata?.evidence) throw new Error("expected gameplay evidence");
|
|
828
|
-
test.metadata.evidence.registeredScenes.push("DebugScene");
|
|
829
|
-
expect(auditGameplayRun(options, [test]).issues).toContain(
|
|
830
|
-
'host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "DebugScene"\u3002'
|
|
831
|
-
);
|
|
832
|
-
});
|
|
833
|
-
it("\u5355\u72EC\u8FD4\u56DE\u53EF\u64CD\u4F5C\u7684 evidence \u7F3A\u53E3", () => {
|
|
834
|
-
const metadata = createGameplayContractMetadata(options.contracts[0]);
|
|
835
|
-
const evidence = createEvidence();
|
|
836
|
-
evidence.mouseEvents = 0;
|
|
837
|
-
evidence.transitions = [];
|
|
838
|
-
expect(
|
|
839
|
-
getGameplayEvidenceIssues(metadata.id, metadata.requirements, evidence)
|
|
840
|
-
).toEqual(
|
|
841
|
-
expect.arrayContaining([
|
|
842
|
-
'contract "gameplay.main-flow" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002',
|
|
843
|
-
'contract "gameplay.main-flow" \u672A\u8BB0\u5F55\u5230 BattleScene \u7684 Scene \u8F6C\u573A\u3002'
|
|
844
|
-
])
|
|
845
|
-
);
|
|
846
|
-
});
|
|
847
|
-
it("\u4EC5\u5728\u5951\u7EA6\u663E\u5F0F\u58F0\u660E\u65F6\u8981\u6C42 Arcade Physics \u8BC1\u636E", () => {
|
|
848
|
-
const evidence = createEvidence();
|
|
849
|
-
expect(getGameplayEvidenceIssues("no-physics", {}, evidence)).not.toContain(
|
|
850
|
-
'contract "no-physics" \u672A\u63A8\u8FDB Arcade Physics\u3002'
|
|
851
|
-
);
|
|
852
|
-
expect(
|
|
853
|
-
getGameplayEvidenceIssues(
|
|
854
|
-
"arcade-physics",
|
|
855
|
-
{ requirePhysicsStep: true },
|
|
856
|
-
evidence
|
|
857
|
-
)
|
|
858
|
-
).toContain('contract "arcade-physics" \u672A\u63A8\u8FDB Arcade Physics\u3002');
|
|
859
|
-
});
|
|
860
|
-
it("\u5408\u5E76\u76F8\u540C\u4FEE\u590D\u7C7B\u578B\u4F46\u4FDD\u7559\u4E0D\u540C Scene \u8F6C\u573A\u76EE\u6807", () => {
|
|
861
|
-
const result = formatGameplayIssues([
|
|
862
|
-
'contract "Menu.start" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002',
|
|
863
|
-
'contract "Battle.start" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002',
|
|
864
|
-
'contract "Menu.to-battle" \u672A\u8BB0\u5F55\u5230 BattleScene \u7684 Scene \u8F6C\u573A\u3002',
|
|
865
|
-
'contract "Battle.to-result" \u672A\u8BB0\u5F55\u5230 ResultScene \u7684 Scene \u8F6C\u573A\u3002',
|
|
866
|
-
'contract "Menu.visit-result" \u672A\u8BBF\u95EE Scene "ResultScene"\u3002',
|
|
867
|
-
'contract "Battle.visit-result" \u672A\u8BBF\u95EE Scene "ResultScene"\u3002'
|
|
868
|
-
]);
|
|
869
|
-
expect(result.issues).toHaveLength(4);
|
|
870
|
-
expect(result.repairs).toHaveLength(3);
|
|
871
|
-
expect(result.repairs[0]).toContain("\u8F93\u5165");
|
|
872
|
-
expect(result.repairs[1]).toContain("\u573A\u666F\u5207\u6362");
|
|
873
|
-
});
|
|
874
|
-
it("\u5C06\u591A\u68C0\u67E5\u9879\u7684\u7F3A\u53E3\u9650\u5236\u4E3A\u95EE\u9898\u7EC4\u800C\u4E0D\u662F\u539F\u59CB\u65E5\u5FD7\u884C", () => {
|
|
875
|
-
const issues = Array.from(
|
|
876
|
-
{ length: 20 },
|
|
877
|
-
(_, index) => `contract "Scene${index}.boot" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`
|
|
878
|
-
);
|
|
879
|
-
const result = formatGameplayIssues(issues);
|
|
880
|
-
expect(result.issues).toHaveLength(1);
|
|
881
|
-
expect(result.repairs).toHaveLength(1);
|
|
882
|
-
expect(result.omitted).toBe(0);
|
|
883
|
-
});
|
|
884
|
-
it("\u5C06\u7F3A\u5931\u6D4B\u8BD5\u5408\u5E76\u4E3A\u4E00\u4E2A\u53EF\u6267\u884C\u4FEE\u590D\u5E76\u9690\u85CF Scene \u7EA7\u8054\u9519\u8BEF", () => {
|
|
885
|
-
const result = formatGameplayIssues([
|
|
886
|
-
'\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "IndexScene.boot" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08tests/production-game.test.ts\uFF09\u3002',
|
|
887
|
-
'\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "BattleScene.playthrough" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08tests/production-game.test.ts\uFF09\u3002',
|
|
888
|
-
'\u7F3A\u5C11 gameplay contract "IndexScene.boot"\u3002',
|
|
889
|
-
'\u7F3A\u5C11 gameplay contract "BattleScene.playthrough"\u3002',
|
|
890
|
-
'\u751F\u4EA7 Scene "BattleScene" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002'
|
|
891
|
-
]);
|
|
892
|
-
expect(result.issues).toEqual([
|
|
893
|
-
"tests/production-game.test.ts \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1AIndexScene.boot\u3001BattleScene.playthrough\u3002"
|
|
894
|
-
]);
|
|
895
|
-
expect(result.repairs).toHaveLength(1);
|
|
896
|
-
expect(result.repairs[0]).toContain("vitest.config.ts");
|
|
897
|
-
});
|
|
898
|
-
it("\u4FDD\u7559\u5E76\u5408\u5E76 Vitest \u8FD0\u884C\u65F6\u5F02\u5E38\u6D88\u606F", () => {
|
|
899
|
-
const result = formatGameplayIssues([
|
|
900
|
-
'\u73A9\u6CD5\u6D4B\u8BD5 "Map.transition" \u6D4B\u8BD5\u5931\u8D25\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events',
|
|
901
|
-
'\u73A9\u6CD5\u6D4B\u8BD5 "Editor.transition" \u6D4B\u8BD5\u5931\u8D25\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events',
|
|
902
|
-
"\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events"
|
|
903
|
-
]);
|
|
904
|
-
expect(result.issues).toEqual([
|
|
905
|
-
'\u73A9\u6CD5\u6D4B\u8BD5 "Map.transition" \u6D4B\u8BD5\u5931\u8D25\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1AEditor.transition\uFF09'
|
|
906
|
-
]);
|
|
907
|
-
expect(result.repairs[0]).toContain("\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF");
|
|
908
|
-
});
|
|
909
|
-
});
|