miaoda-game-devkit 0.1.0 → 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.
@@ -0,0 +1,36 @@
1
+ // src/lint/vitest.config.ts
2
+ import { dirname, resolve } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { defineConfig } from "vitest/config";
5
+ var phaserPackageRoot = dirname(
6
+ fileURLToPath(import.meta.resolve("phaser/package.json"))
7
+ );
8
+ var vitest_config_default = defineConfig({
9
+ resolve: {
10
+ alias: {
11
+ phaser: resolve(phaserPackageRoot, "dist/phaser.js")
12
+ }
13
+ },
14
+ test: {
15
+ server: {
16
+ deps: {
17
+ inline: [/miaoda-game-/]
18
+ }
19
+ },
20
+ environment: "jsdom",
21
+ environmentOptions: {
22
+ jsdom: {
23
+ url: "http://localhost/"
24
+ }
25
+ },
26
+ include: [resolve(import.meta.dirname, "*.contract.mjs")],
27
+ // 发布后的契约位于使用方的 node_modules 目录中。
28
+ exclude: [],
29
+ setupFiles: [resolve(import.meta.dirname, "setup.mjs")],
30
+ restoreMocks: true,
31
+ clearMocks: true
32
+ }
33
+ });
34
+ export {
35
+ vitest_config_default as default
36
+ };
@@ -0,0 +1,426 @@
1
+ // src/lint/gameplay-audit.test.ts
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ // src/gameplay-audit.ts
5
+ function normalizeList(value) {
6
+ if (value === void 0) return [];
7
+ const values = typeof value === "string" ? [value] : value;
8
+ return [...new Set(values.map((item) => item.trim()).filter(Boolean))];
9
+ }
10
+ function normalizeSet(values) {
11
+ return [
12
+ ...new Set(values.map((value) => value.trim()).filter(Boolean))
13
+ ].sort();
14
+ }
15
+ function sameSet(left, right) {
16
+ return JSON.stringify(normalizeSet(left)) === JSON.stringify(normalizeSet(right));
17
+ }
18
+ function normalizeGameplayRequirements(requirements, scenes) {
19
+ const requiredScenes = normalizeSet([
20
+ ...scenes,
21
+ ...normalizeList(requirements.requireScene)
22
+ ]);
23
+ return {
24
+ requireInput: requirements.requireInput || void 0,
25
+ requireFrameAdvance: requirements.requireFrameAdvance || void 0,
26
+ requirePhysicsStep: requirements.requirePhysicsStep || void 0,
27
+ requireTransition: normalizeList(requirements.requireTransition),
28
+ requireScene: requiredScenes,
29
+ requireRestart: normalizeList(requirements.requireRestart),
30
+ requireCheckpoint: normalizeList(requirements.requireCheckpoint),
31
+ requireDestroy: true
32
+ };
33
+ }
34
+ function createGameplayContractMetadata(contract) {
35
+ const scenes = normalizeSet(contract.scenes);
36
+ return {
37
+ id: contract.id.trim(),
38
+ scenes,
39
+ requirements: normalizeGameplayRequirements(contract, scenes),
40
+ verified: false
41
+ };
42
+ }
43
+ function validateGameplayAuditOptions(options2) {
44
+ const issues = [];
45
+ const scenes = normalizeSet(options2.scenes);
46
+ if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
47
+ if (scenes.length !== options2.scenes.length) {
48
+ issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
49
+ }
50
+ const contractIds = /* @__PURE__ */ new Set();
51
+ const coveredScenes = /* @__PURE__ */ new Set();
52
+ for (const contract of options2.contracts) {
53
+ const id = contract.id.trim();
54
+ if (!id) {
55
+ issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
56
+ } else if (id !== contract.id) {
57
+ issues.push(`contract id "${contract.id}" \u4E0D\u80FD\u5305\u542B\u9996\u5C3E\u7A7A\u767D\u3002`);
58
+ } else if (contractIds.has(id)) {
59
+ issues.push(`gameplayAudit.contracts \u5305\u542B\u91CD\u590D contract id\uFF1A${id}\u3002`);
60
+ } else {
61
+ contractIds.add(id);
62
+ }
63
+ const testFile = contract.testFile.replaceAll("\\", "/");
64
+ if (!/^tests\/.+\.test\.ts$/.test(testFile) || testFile.split("/").includes("..")) {
65
+ issues.push(
66
+ `contract "${id || "<empty>"}" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002`
67
+ );
68
+ }
69
+ const contractScenes = normalizeSet(contract.scenes);
70
+ if (contractScenes.length === 0) {
71
+ issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
72
+ }
73
+ for (const scene of contractScenes) {
74
+ coveredScenes.add(scene);
75
+ if (!scenes.includes(scene)) {
76
+ issues.push(
77
+ `contract "${id}" \u5F15\u7528\u4E86\u672A\u5217\u5165 gameplayAudit.scenes \u7684 Scene\uFF1A${scene}\u3002`
78
+ );
79
+ }
80
+ }
81
+ }
82
+ for (const scene of scenes) {
83
+ if (!coveredScenes.has(scene)) {
84
+ issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
85
+ }
86
+ }
87
+ if (options2.contracts.length === 0)
88
+ issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
89
+ return issues;
90
+ }
91
+ function getDefinitionIssues(expected, actual) {
92
+ const issues = [];
93
+ const expectedMetadata = createGameplayContractMetadata(expected);
94
+ if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
95
+ issues.push(
96
+ `contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
97
+ );
98
+ }
99
+ if (JSON.stringify(expectedMetadata.requirements) !== JSON.stringify(actual.requirements)) {
100
+ issues.push(
101
+ `contract "${expected.id}" \u7684 evidence requirements \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
102
+ );
103
+ }
104
+ return issues;
105
+ }
106
+ function auditGameplayCollection(options2, file, tests) {
107
+ const normalizedFile = file.replaceAll("\\", "/");
108
+ const expected = options2.contracts.filter(
109
+ (contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
110
+ );
111
+ const actual = tests.filter((test) => test.metadata);
112
+ const issues = [];
113
+ for (const contract of expected) {
114
+ const matches = actual.filter((test) => test.metadata?.id === contract.id);
115
+ if (matches.length === 0) {
116
+ issues.push(
117
+ `\u6D4B\u8BD5\u6267\u884C\u524D\u672A\u6536\u96C6\u5230 contract "${contract.id}"\uFF08${normalizedFile}\uFF09\u3002`
118
+ );
119
+ } else if (matches.length > 1) {
120
+ issues.push(
121
+ `contract "${contract.id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`
122
+ );
123
+ } else if (matches[0]?.metadata) {
124
+ issues.push(...getDefinitionIssues(contract, matches[0].metadata));
125
+ }
126
+ }
127
+ for (const test of actual) {
128
+ const id = test.metadata?.id;
129
+ if (!id) continue;
130
+ const expectedContract = options2.contracts.find(
131
+ (contract) => contract.id === id
132
+ );
133
+ if (!expectedContract) {
134
+ issues.push(
135
+ `\u6D4B\u8BD5 "${test.name}" \u58F0\u660E\u4E86\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\u3002`
136
+ );
137
+ } else if (expectedContract.testFile.replaceAll("\\", "/") !== normalizedFile) {
138
+ issues.push(
139
+ `contract "${id}" \u5FC5\u987B\u4F4D\u4E8E ${expectedContract.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${normalizedFile}\u3002`
140
+ );
141
+ }
142
+ }
143
+ return { passed: issues.length === 0, issues };
144
+ }
145
+ function getGameplayEvidenceIssues(id, requirements, evidence) {
146
+ const issues = [];
147
+ const inputEvents = evidence.mouseEvents + evidence.keyboardEvents + evidence.touchEvents;
148
+ if (requirements.requireInput && inputEvents === 0)
149
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002`);
150
+ if (requirements.requireFrameAdvance && evidence.frames === 0)
151
+ issues.push(`contract "${id}" \u672A\u63A8\u8FDB\u5B8C\u6574 Phaser \u5E27\u3002`);
152
+ if (requirements.requirePhysicsStep && evidence.physicsSteps === 0)
153
+ issues.push(`contract "${id}" \u672A\u63A8\u8FDB Arcade Physics\u3002`);
154
+ for (const target of normalizeList(requirements.requireTransition)) {
155
+ if (!evidence.transitions.some((transition) => transition.to === target)) {
156
+ issues.push(`contract "${id}" \u672A\u8BB0\u5F55\u5230 ${target} \u7684 Scene \u8F6C\u573A\u3002`);
157
+ }
158
+ }
159
+ for (const scene of normalizeList(requirements.requireScene)) {
160
+ if (!evidence.visitedScenes.includes(scene))
161
+ issues.push(`contract "${id}" \u672A\u8BBF\u95EE Scene "${scene}"\u3002`);
162
+ }
163
+ for (const scene of normalizeList(requirements.requireRestart)) {
164
+ if (!evidence.restartedScenes.includes(scene))
165
+ issues.push(`contract "${id}" \u672A\u91CD\u542F Scene "${scene}"\u3002`);
166
+ }
167
+ for (const checkpoint of normalizeList(requirements.requireCheckpoint)) {
168
+ if (!evidence.checkpoints.includes(checkpoint))
169
+ issues.push(`contract "${id}" \u7F3A\u5C11 checkpoint "${checkpoint}"\u3002`);
170
+ }
171
+ if (requirements.requireDestroy && !evidence.destroyed)
172
+ issues.push(`contract "${id}" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002`);
173
+ return issues;
174
+ }
175
+ function auditGameplayRun(options2, tests) {
176
+ const issues = [];
177
+ const actualContracts = tests.filter((test) => test.metadata);
178
+ const acceptedEvidence = [];
179
+ for (const expected of options2.contracts) {
180
+ const matches = actualContracts.filter(
181
+ (test2) => test2.metadata?.id === expected.id
182
+ );
183
+ if (matches.length === 0) {
184
+ issues.push(`\u7F3A\u5C11 gameplay contract "${expected.id}"\u3002`);
185
+ continue;
186
+ }
187
+ if (matches.length > 1) {
188
+ issues.push(`gameplay contract "${expected.id}" \u88AB\u591A\u4E2A\u6D4B\u8BD5\u91CD\u590D\u58F0\u660E\u3002`);
189
+ continue;
190
+ }
191
+ const test = matches[0];
192
+ const metadata = test?.metadata;
193
+ if (!test || !metadata) continue;
194
+ if (test.file.replaceAll("\\", "/") !== expected.testFile.replaceAll("\\", "/")) {
195
+ issues.push(
196
+ `contract "${expected.id}" \u5FC5\u987B\u4F4D\u4E8E ${expected.testFile}\uFF0C\u5B9E\u9645\u4F4D\u4E8E ${test.file}\u3002`
197
+ );
198
+ }
199
+ issues.push(...getDefinitionIssues(expected, metadata));
200
+ if (test.state !== "passed") {
201
+ issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
202
+ continue;
203
+ }
204
+ if (!metadata.verified || !metadata.evidence) {
205
+ issues.push(`contract "${expected.id}" \u672A\u5B8C\u6210 onTestFinished \u8BC1\u636E\u6821\u9A8C\u3002`);
206
+ continue;
207
+ }
208
+ const evidenceIssues = getGameplayEvidenceIssues(
209
+ expected.id,
210
+ metadata.requirements,
211
+ metadata.evidence
212
+ );
213
+ issues.push(...evidenceIssues);
214
+ if (evidenceIssues.length === 0) acceptedEvidence.push(metadata.evidence);
215
+ }
216
+ for (const test of actualContracts) {
217
+ const id = test.metadata?.id;
218
+ if (id && !options2.contracts.some((contract) => contract.id === id)) {
219
+ issues.push(
220
+ `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
221
+ );
222
+ }
223
+ }
224
+ const registeredScenes = normalizeSet(
225
+ acceptedEvidence.flatMap((evidence) => evidence.registeredScenes)
226
+ );
227
+ const expectedScenes = normalizeSet(options2.scenes);
228
+ for (const scene of expectedScenes) {
229
+ if (!registeredScenes.includes(scene))
230
+ issues.push(
231
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u4EFB\u4F55\u901A\u8FC7\u5BA1\u8BA1\u7684 host registry \u4E2D\u3002`
232
+ );
233
+ }
234
+ for (const scene of registeredScenes) {
235
+ if (!expectedScenes.includes(scene))
236
+ issues.push(
237
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
238
+ );
239
+ }
240
+ return { passed: issues.length === 0, issues: [...new Set(issues)] };
241
+ }
242
+
243
+ // src/lint/gameplay-audit.test.ts
244
+ var options = {
245
+ scenes: ["IndexScene", "BattleScene"],
246
+ contracts: [
247
+ {
248
+ id: "gameplay.main-flow",
249
+ testFile: "tests/gameplay.test.ts",
250
+ scenes: ["IndexScene", "BattleScene"],
251
+ requireInput: true,
252
+ requireFrameAdvance: true,
253
+ requireTransition: "BattleScene",
254
+ requireCheckpoint: ["progress", "recovery"],
255
+ requireRestart: "BattleScene"
256
+ }
257
+ ]
258
+ };
259
+ function createEvidence() {
260
+ return {
261
+ frames: 3,
262
+ physicsSteps: 0,
263
+ mouseEvents: 3,
264
+ keyboardEvents: 0,
265
+ touchEvents: 0,
266
+ clicks: 1,
267
+ registeredScenes: ["IndexScene", "BattleScene"],
268
+ visitedScenes: ["IndexScene", "BattleScene"],
269
+ restartedScenes: ["BattleScene"],
270
+ checkpoints: ["progress", "recovery"],
271
+ destroyed: true,
272
+ transitions: [{ from: "IndexScene", to: "BattleScene", method: "start" }]
273
+ };
274
+ }
275
+ function createPassingTest() {
276
+ const metadata = createGameplayContractMetadata(options.contracts[0]);
277
+ metadata.evidence = createEvidence();
278
+ metadata.verified = true;
279
+ return {
280
+ file: "tests/gameplay.test.ts",
281
+ name: "main gameplay flow",
282
+ state: "passed",
283
+ metadata
284
+ };
285
+ }
286
+ describe("gameplay audit", () => {
287
+ it("\u62D2\u7EDD\u672A\u88AB contract \u8986\u76D6\u7684\u751F\u4EA7 Scene", () => {
288
+ expect(
289
+ validateGameplayAuditOptions({
290
+ scenes: ["IndexScene", "ResultScene"],
291
+ contracts: [options.contracts[0]]
292
+ })
293
+ ).toContain('Scene "ResultScene" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002');
294
+ });
295
+ it("\u62D2\u7EDD\u4F1A\u5728 metadata \u4E2D\u88AB\u9759\u9ED8\u6539\u5199\u7684 contract id", () => {
296
+ expect(
297
+ validateGameplayAuditOptions({
298
+ scenes: ["IndexScene"],
299
+ contracts: [
300
+ {
301
+ id: " IndexScene.boot ",
302
+ testFile: "tests/gameplay.test.ts",
303
+ scenes: ["IndexScene"]
304
+ }
305
+ ]
306
+ })
307
+ ).toContain('contract id " IndexScene.boot " \u4E0D\u80FD\u5305\u542B\u9996\u5C3E\u7A7A\u767D\u3002');
308
+ });
309
+ it("\u8BA9 contract testFile \u4E0E\u5171\u4EAB Vitest \u6D4B\u8BD5\u8303\u56F4\u4FDD\u6301\u4E00\u81F4", () => {
310
+ const createOptions = (testFile) => ({
311
+ scenes: ["IndexScene"],
312
+ contracts: [
313
+ {
314
+ id: "IndexScene.boot",
315
+ testFile,
316
+ scenes: ["IndexScene"]
317
+ }
318
+ ]
319
+ });
320
+ expect(
321
+ validateGameplayAuditOptions(createOptions("tests/gameplay.test.tsx"))
322
+ ).toContain(
323
+ 'contract "IndexScene.boot" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002'
324
+ );
325
+ expect(
326
+ validateGameplayAuditOptions(
327
+ createOptions("tests/flows/flow..recovery.test.ts")
328
+ )
329
+ ).not.toContain(
330
+ 'contract "IndexScene.boot" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002'
331
+ );
332
+ expect(
333
+ validateGameplayAuditOptions(createOptions("tests/../outside.test.ts"))
334
+ ).toContain(
335
+ 'contract "IndexScene.boot" \u7684 testFile \u5FC5\u987B\u5339\u914D tests/**/*.test.ts\u3002'
336
+ );
337
+ });
338
+ it("\u5728\u6D4B\u8BD5\u6267\u884C\u524D\u62A5\u544A\u672A\u6536\u96C6\u5230\u7684\u9759\u6001 contract", () => {
339
+ const result = auditGameplayCollection(
340
+ options,
341
+ "tests/gameplay.test.ts",
342
+ []
343
+ );
344
+ expect(result).toEqual({
345
+ passed: false,
346
+ issues: [
347
+ '\u6D4B\u8BD5\u6267\u884C\u524D\u672A\u6536\u96C6\u5230 contract "gameplay.main-flow"\uFF08tests/gameplay.test.ts\uFF09\u3002'
348
+ ]
349
+ });
350
+ });
351
+ it("\u5728\u6D4B\u8BD5\u6267\u884C\u524D\u62A5\u544A\u5199\u5728\u9519\u8BEF\u6587\u4EF6\u4E2D\u7684 contract", () => {
352
+ const metadata = createGameplayContractMetadata(options.contracts[0]);
353
+ const result = auditGameplayCollection(options, "tests/wrong.test.ts", [
354
+ {
355
+ file: "tests/wrong.test.ts",
356
+ name: "misplaced gameplay flow",
357
+ state: "pending",
358
+ metadata
359
+ }
360
+ ]);
361
+ expect(result).toEqual({
362
+ passed: false,
363
+ issues: [
364
+ 'contract "gameplay.main-flow" \u5FC5\u987B\u4F4D\u4E8E tests/gameplay.test.ts\uFF0C\u5B9E\u9645\u4F4D\u4E8E tests/wrong.test.ts\u3002'
365
+ ]
366
+ });
367
+ });
368
+ it("\u63A5\u53D7\u5B9A\u4E49\u3001Ledger \u548C Scene registry \u90FD\u5B8C\u6574\u7684\u6D4B\u8BD5\u8FD0\u884C", () => {
369
+ expect(auditGameplayRun(options, [createPassingTest()])).toEqual({
370
+ passed: true,
371
+ issues: []
372
+ });
373
+ });
374
+ it("\u6C47\u603B checkpoint\u3001restart\u3001destroy \u548C\u989D\u5916 Scene \u7F3A\u53E3", () => {
375
+ const evidence = createEvidence();
376
+ evidence.checkpoints = ["progress"];
377
+ evidence.restartedScenes = [];
378
+ evidence.destroyed = false;
379
+ evidence.registeredScenes.push("DebugScene");
380
+ const test = createPassingTest();
381
+ test.metadata.evidence = evidence;
382
+ const result = auditGameplayRun(options, [test]);
383
+ expect(result.passed).toBe(false);
384
+ expect(result.issues).toEqual(
385
+ expect.arrayContaining([
386
+ 'contract "gameplay.main-flow" \u672A\u91CD\u542F Scene "BattleScene"\u3002',
387
+ 'contract "gameplay.main-flow" \u7F3A\u5C11 checkpoint "recovery"\u3002',
388
+ 'contract "gameplay.main-flow" \u672A\u5B8C\u6210 HEADLESS host \u9500\u6BC1\u3002'
389
+ ])
390
+ );
391
+ });
392
+ it("\u62D2\u7EDD\u901A\u8FC7\u5951\u7EA6\u7684 host registry \u4E2D\u51FA\u73B0\u6E05\u5355\u5916 Scene", () => {
393
+ const test = createPassingTest();
394
+ test.metadata.evidence.registeredScenes.push("DebugScene");
395
+ expect(auditGameplayRun(options, [test]).issues).toContain(
396
+ 'host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "DebugScene"\u3002'
397
+ );
398
+ });
399
+ it("\u5355\u72EC\u8FD4\u56DE\u53EF\u64CD\u4F5C\u7684 evidence \u7F3A\u53E3", () => {
400
+ const metadata = createGameplayContractMetadata(options.contracts[0]);
401
+ const evidence = createEvidence();
402
+ evidence.mouseEvents = 0;
403
+ evidence.transitions = [];
404
+ expect(
405
+ getGameplayEvidenceIssues(metadata.id, metadata.requirements, evidence)
406
+ ).toEqual(
407
+ expect.arrayContaining([
408
+ 'contract "gameplay.main-flow" \u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165\u3002',
409
+ 'contract "gameplay.main-flow" \u672A\u8BB0\u5F55\u5230 BattleScene \u7684 Scene \u8F6C\u573A\u3002'
410
+ ])
411
+ );
412
+ });
413
+ it("\u4EC5\u5728\u5951\u7EA6\u663E\u5F0F\u58F0\u660E\u65F6\u8981\u6C42 Arcade Physics \u8BC1\u636E", () => {
414
+ const evidence = createEvidence();
415
+ expect(getGameplayEvidenceIssues("no-physics", {}, evidence)).not.toContain(
416
+ 'contract "no-physics" \u672A\u63A8\u8FDB Arcade Physics\u3002'
417
+ );
418
+ expect(
419
+ getGameplayEvidenceIssues(
420
+ "arcade-physics",
421
+ { requirePhysicsStep: true },
422
+ evidence
423
+ )
424
+ ).toContain('contract "arcade-physics" \u672A\u63A8\u8FDB Arcade Physics\u3002');
425
+ });
426
+ });