miaoda-game-devkit 0.1.1 → 0.2.0

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.
@@ -24,17 +24,352 @@ __export(vitest_config_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(vitest_config_exports);
26
26
  var import_node_module = require("module");
27
- var import_node_path = require("path");
27
+ var import_node_path2 = require("path");
28
28
  var import_config = require("vitest/config");
29
+
30
+ // src/gameplay-audit.ts
31
+ function normalizeList(value) {
32
+ if (value === void 0) return [];
33
+ const values = typeof value === "string" ? [value] : value;
34
+ return [...new Set(values.map((item) => item.trim()).filter(Boolean))];
35
+ }
36
+ function normalizeSet(values) {
37
+ return [
38
+ ...new Set(values.map((value) => value.trim()).filter(Boolean))
39
+ ].sort();
40
+ }
41
+ function sameSet(left, right) {
42
+ return JSON.stringify(normalizeSet(left)) === JSON.stringify(normalizeSet(right));
43
+ }
44
+ function normalizeGameplayRequirements(requirements, scenes) {
45
+ const requiredScenes = normalizeSet([
46
+ ...scenes,
47
+ ...normalizeList(requirements.requireScene)
48
+ ]);
49
+ return {
50
+ requireInput: requirements.requireInput || void 0,
51
+ requireFrameAdvance: requirements.requireFrameAdvance || void 0,
52
+ requirePhysicsStep: requirements.requirePhysicsStep || void 0,
53
+ requireTransition: normalizeList(requirements.requireTransition),
54
+ requireScene: requiredScenes,
55
+ requireRestart: normalizeList(requirements.requireRestart),
56
+ requireCheckpoint: normalizeList(requirements.requireCheckpoint),
57
+ requireDestroy: true
58
+ };
59
+ }
60
+ function createGameplayContractMetadata(contract) {
61
+ const scenes = normalizeSet(contract.scenes);
62
+ return {
63
+ id: contract.id.trim(),
64
+ scenes,
65
+ requirements: normalizeGameplayRequirements(contract, scenes),
66
+ verified: false
67
+ };
68
+ }
69
+ function validateGameplayAuditOptions(options) {
70
+ const issues = [];
71
+ const scenes = normalizeSet(options.scenes);
72
+ if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
73
+ if (scenes.length !== options.scenes.length) {
74
+ issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
75
+ }
76
+ const contractIds = /* @__PURE__ */ new Set();
77
+ const coveredScenes = /* @__PURE__ */ new Set();
78
+ for (const contract of options.contracts) {
79
+ const id = contract.id.trim();
80
+ if (!id) {
81
+ issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
82
+ } else if (id !== contract.id) {
83
+ issues.push(`contract id "${contract.id}" \u4E0D\u80FD\u5305\u542B\u9996\u5C3E\u7A7A\u767D\u3002`);
84
+ } else if (contractIds.has(id)) {
85
+ issues.push(`gameplayAudit.contracts \u5305\u542B\u91CD\u590D contract id\uFF1A${id}\u3002`);
86
+ } else {
87
+ contractIds.add(id);
88
+ }
89
+ const testFile = contract.testFile.replaceAll("\\", "/");
90
+ if (!/^tests\/.+\.test\.ts$/.test(testFile) || testFile.split("/").includes("..")) {
91
+ issues.push(
92
+ `contract "${id || "<empty>"}" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002`
93
+ );
94
+ }
95
+ const contractScenes = normalizeSet(contract.scenes);
96
+ if (contractScenes.length === 0) {
97
+ issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
98
+ }
99
+ for (const scene of contractScenes) {
100
+ coveredScenes.add(scene);
101
+ if (!scenes.includes(scene)) {
102
+ issues.push(
103
+ `contract "${id}" \u5F15\u7528\u4E86\u672A\u5217\u5165 gameplayAudit.scenes \u7684 Scene\uFF1A${scene}\u3002`
104
+ );
105
+ }
106
+ }
107
+ }
108
+ for (const scene of scenes) {
109
+ if (!coveredScenes.has(scene)) {
110
+ issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
111
+ }
112
+ }
113
+ if (options.contracts.length === 0)
114
+ issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
115
+ return issues;
116
+ }
117
+ function getDefinitionIssues(expected, actual) {
118
+ const issues = [];
119
+ const expectedMetadata = createGameplayContractMetadata(expected);
120
+ if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
121
+ issues.push(
122
+ `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
123
+ );
124
+ }
125
+ if (JSON.stringify(expectedMetadata.requirements) !== JSON.stringify(actual.requirements)) {
126
+ issues.push(
127
+ `contract "${expected.id}" \u7684 evidence requirements \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
128
+ );
129
+ }
130
+ return issues;
131
+ }
132
+ function auditGameplayCollection(options, file, tests) {
133
+ const normalizedFile = file.replaceAll("\\", "/");
134
+ const expected = options.contracts.filter(
135
+ (contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
136
+ );
137
+ const actual = tests.filter((test) => test.metadata);
138
+ const issues = [];
139
+ for (const contract of expected) {
140
+ const matches = actual.filter((test) => test.metadata?.id === contract.id);
141
+ if (matches.length === 0) {
142
+ issues.push(
143
+ `\u6D4B\u8BD5\u6267\u884C\u524D\u672A\u6536\u96C6\u5230 contract "${contract.id}"\uFF08${normalizedFile}\uFF09\u3002`
144
+ );
145
+ } else if (matches.length > 1) {
146
+ issues.push(
147
+ `contract "${contract.id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`
148
+ );
149
+ } else if (matches[0]?.metadata) {
150
+ issues.push(...getDefinitionIssues(contract, matches[0].metadata));
151
+ }
152
+ }
153
+ for (const test of actual) {
154
+ const id = test.metadata?.id;
155
+ if (!id) continue;
156
+ const expectedContract = options.contracts.find(
157
+ (contract) => contract.id === id
158
+ );
159
+ if (!expectedContract) {
160
+ issues.push(
161
+ `\u6D4B\u8BD5 "${test.name}" \u58F0\u660E\u4E86\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\u3002`
162
+ );
163
+ } else if (expectedContract.testFile.replaceAll("\\", "/") !== normalizedFile) {
164
+ issues.push(
165
+ `contract "${id}" \u5FC5\u987B\u4F4D\u4E8E ${expectedContract.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${normalizedFile}\u3002`
166
+ );
167
+ }
168
+ }
169
+ return { passed: issues.length === 0, issues };
170
+ }
171
+ function getGameplayEvidenceIssues(id, requirements, evidence) {
172
+ const issues = [];
173
+ const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
174
+ if (requirements.requireInput && inputEvents === 0)
175
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
176
+ if (requirements.requireFrameAdvance && evidence.frames === 0)
177
+ issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
178
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
179
+ issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
180
+ for (const target of normalizeList(requirements.requireTransition)) {
181
+ if (!evidence.transitions.some((transition) => transition.to === target)) {
182
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
183
+ }
184
+ }
185
+ for (const scene of normalizeList(requirements.requireScene)) {
186
+ if (!evidence.visitedScenes.includes(scene))
187
+ issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
188
+ }
189
+ for (const scene of normalizeList(requirements.requireRestart)) {
190
+ if (!evidence.restartedScenes.includes(scene))
191
+ issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
192
+ }
193
+ for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
194
+ if (!evidence.checkpoints.includes(checkpoint))
195
+ issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
196
+ }
197
+ if (requirements.requireDestroy && !evidence.destroyed)
198
+ issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
199
+ return issues;
200
+ }
201
+ function auditGameplayRun(options, tests) {
202
+ const issues = [];
203
+ const actualContracts = tests.filter((test) => test.metadata);
204
+ const acceptedEvidence = [];
205
+ for (const expected of options.contracts) {
206
+ const matches = actualContracts.filter(
207
+ (test2) => test2.metadata?.id === expected.id
208
+ );
209
+ if (matches.length === 0) {
210
+ issues.push(`\u7F3A\u5C11 gameplay contract "${expected.id}"\u3002`);
211
+ continue;
212
+ }
213
+ if (matches.length > 1) {
214
+ issues.push(`gameplay contract "${expected.id}" \u88AB\u591A\u4E2A\u6D4B\u8BD5\u91CD\u590D\u58F0\u660E\u3002`);
215
+ continue;
216
+ }
217
+ const test = matches[0];
218
+ const metadata = test?.metadata;
219
+ if (!test || !metadata) continue;
220
+ if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
221
+ issues.push(
222
+ `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
223
+ );
224
+ }
225
+ issues.push(...getDefinitionIssues(expected, metadata));
226
+ if (test.state !== "passed") {
227
+ issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
228
+ continue;
229
+ }
230
+ if (!metadata.verified || !metadata.evidence) {
231
+ issues.push(`contract "${expected.id}" \u672A\u5B8C\u6210 onTestFinished \u8BC1\u636E\u6821\u9A8C\u3002`);
232
+ continue;
233
+ }
234
+ const evidenceIssues = getGameplayEvidenceIssues(
235
+ expected.id,
236
+ metadata.requirements,
237
+ metadata.evidence
238
+ );
239
+ issues.push(...evidenceIssues);
240
+ if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
241
+ }
242
+ for (const test of actualContracts) {
243
+ const id = test.metadata?.id;
244
+ if (id && !options.contracts.some((contract) => contract.id === id)) {
245
+ issues.push(
246
+ `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
247
+ );
248
+ }
249
+ }
250
+ const registeredScenes = normalizeSet(
251
+ acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
252
+ );
253
+ const expectedScenes = normalizeSet(options.scenes);
254
+ for (const scene of expectedScenes) {
255
+ if (!registeredScenes.includes(scene))
256
+ issues.push(
257
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u4EFB\u4F55\u901A\u8FC7\u5BA1\u8BA1\u7684 host registry \u4E2D\u3002`
258
+ );
259
+ }
260
+ for (const scene of registeredScenes) {
261
+ if (!expectedScenes.includes(scene))
262
+ issues.push(
263
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
264
+ );
265
+ }
266
+ return { passed: issues.length === 0, issues: [...new Set(issues)] };
267
+ }
268
+
269
+ // src/gameplay-audit-reporter.ts
270
+ var import_node_path = require("path");
271
+ function normalizePath(path) {
272
+ return path.replaceAll("\\", "/").replace(/^\.\//, "");
273
+ }
274
+ function isGameplayContractMetadata(value) {
275
+ if (!value || typeof value !== "object") return false;
276
+ const record = value;
277
+ return typeof record.id === "string" && Array.isArray(record.scenes) && Boolean(record.requirements) && typeof record.requirements === "object";
278
+ }
279
+ function toAuditTest(test) {
280
+ const metadata = test.meta().gameplayContract;
281
+ return {
282
+ file: normalizePath(test.module.relativeModuleId),
283
+ name: test.fullName,
284
+ state: test.result().state,
285
+ metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
286
+ };
287
+ }
288
+ function printIssues(stage, issues) {
289
+ if (issues.length === 0) return;
290
+ console.error(`
291
+ GAMEPLAY_AUDIT: ${stage} FAILED`);
292
+ for (const issue of issues) console.error(`- ${issue}`);
293
+ }
294
+ var GameplayAuditReporter = class {
295
+ projectRoot;
296
+ options;
297
+ preflightIssues = /* @__PURE__ */ new Set();
298
+ /** 创建绑定到单个游戏项目根目录和权威清单的 reporter。 */
299
+ constructor(projectRoot, options) {
300
+ this.projectRoot = projectRoot;
301
+ this.options = options;
302
+ }
303
+ /** 每次 Vitest 运行开始时重置预检状态,并检查清单中的测试文件是否会执行。 */
304
+ onTestRunStart(specifications) {
305
+ this.preflightIssues.clear();
306
+ const scheduledFiles = new Set(
307
+ specifications.map(
308
+ (specification) => normalizePath((0, import_node_path.relative)(this.projectRoot, specification.moduleId))
309
+ )
310
+ );
311
+ for (const contract of this.options.contracts) {
312
+ const testFile = normalizePath(contract.testFile);
313
+ if (!scheduledFiles.has(testFile)) {
314
+ this.reportPreflightIssue(
315
+ `contract "${contract.id}" \u7684\u6D4B\u8BD5\u6587\u4EF6\u672A\u8FDB\u5165\u672C\u6B21 Vitest \u8FD0\u884C\uFF1A${testFile}\u3002`
316
+ );
317
+ }
318
+ }
319
+ }
320
+ /** 在测试文件执行前检查静态 metadata 是否声明了该文件负责的全部 contract。 */
321
+ onTestModuleCollected(testModule) {
322
+ const file = normalizePath(testModule.relativeModuleId);
323
+ const tests = [...testModule.children.allTests()].map(toAuditTest);
324
+ const result = auditGameplayCollection(this.options, file, tests);
325
+ for (const issue of result.issues) this.reportPreflightIssue(issue);
326
+ }
327
+ /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
328
+ onTestRunEnd(testModules) {
329
+ const tests = testModules.flatMap(
330
+ (testModule) => [...testModule.children.allTests()].map(toAuditTest)
331
+ );
332
+ const result = auditGameplayRun(this.options, tests);
333
+ if (!result.passed) {
334
+ printIssues("FINAL", result.issues);
335
+ process.exitCode = 1;
336
+ } else {
337
+ console.log(
338
+ `
339
+ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
340
+ );
341
+ }
342
+ }
343
+ /** 立即输出一个此前未报告过的预检缺口,并让本次测试命令失败。 */
344
+ reportPreflightIssue(issue) {
345
+ if (this.preflightIssues.has(issue)) return;
346
+ this.preflightIssues.add(issue);
347
+ printIssues("PREFLIGHT", [issue]);
348
+ process.exitCode = 1;
349
+ }
350
+ };
351
+
352
+ // src/vitest-config.ts
29
353
  function defineGameVitestConfig(options) {
30
- const projectRequire = (0, import_node_module.createRequire)((0, import_node_path.resolve)(options.projectRoot, "package.json"));
31
- const phaserPackageRoot = (0, import_node_path.dirname)(projectRequire.resolve("phaser/package.json"));
32
- const phaserBrowserBuild = (0, import_node_path.resolve)(phaserPackageRoot, "dist/phaser.js");
354
+ const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
355
+ if (auditIssues.length > 0) {
356
+ throw new Error(
357
+ `\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
358
+ - ${auditIssues.join("\n- ")}`
359
+ );
360
+ }
361
+ const projectRequire = (0, import_node_module.createRequire)(
362
+ (0, import_node_path2.resolve)(options.projectRoot, "package.json")
363
+ );
364
+ const phaserPackageRoot = (0, import_node_path2.dirname)(
365
+ projectRequire.resolve("phaser/package.json")
366
+ );
367
+ const phaserBrowserBuild = (0, import_node_path2.resolve)(phaserPackageRoot, "dist/phaser.js");
33
368
  return (0, import_config.defineConfig)({
34
369
  resolve: {
35
370
  alias: {
36
371
  ...options.aliases,
37
- "@": (0, import_node_path.resolve)(options.projectRoot, "src"),
372
+ "@": (0, import_node_path2.resolve)(options.projectRoot, "src"),
38
373
  phaser: phaserBrowserBuild
39
374
  }
40
375
  },
@@ -57,6 +392,10 @@ function defineGameVitestConfig(options) {
57
392
  "miaoda-game-devkit/vitest-setup",
58
393
  ...options.additionalSetupFiles ?? []
59
394
  ],
395
+ reporters: [
396
+ "default",
397
+ new GameplayAuditReporter(options.projectRoot, options.gameplayAudit)
398
+ ],
60
399
  restoreMocks: true,
61
400
  clearMocks: true,
62
401
  coverage: {