miaoda-game-devkit 0.2.2 → 0.2.3
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/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 +226 -2
- package/dist/lint/gameplay-contract.contract.mjs +3 -0
- package/dist/lint/vitest-config.contract.mjs +121 -3
- package/dist/vitest-config.d.mts +1 -1
- package/dist/vitest-config.d.ts +1 -1
- package/dist/vitest-config.js +110 -7
- package/dist/vitest-config.mjs +106 -3
- package/package.json +28 -1
|
@@ -162,6 +162,8 @@ declare function createHeadlessGame<TScene extends Phaser.Scene>(scene: TScene,
|
|
|
162
162
|
|
|
163
163
|
/** 一个必须由指定测试文件证明的玩法契约。 */
|
|
164
164
|
interface GameplayAuditContract extends HeadlessGameplayEvidenceRequirements {
|
|
165
|
+
/** 契约用途;boot 不得替代交互游戏的 playthrough。 */
|
|
166
|
+
kind: "boot" | "playthrough" | "transition" | "recovery";
|
|
165
167
|
/** 在整个游戏项目中唯一且稳定的契约标识。 */
|
|
166
168
|
id: string;
|
|
167
169
|
/** 相对于游戏项目根目录的 Vitest 测试文件。 */
|
|
@@ -171,6 +173,15 @@ interface GameplayAuditContract extends HeadlessGameplayEvidenceRequirements {
|
|
|
171
173
|
}
|
|
172
174
|
/** 游戏项目在一次 Vitest 运行中必须完成的玩法审计清单。 */
|
|
173
175
|
interface GameplayAuditOptions {
|
|
176
|
+
/** 默认按交互游戏审计;纯演示项目必须显式豁免并说明原因。 */
|
|
177
|
+
mode?: "interactive" | "non-interactive";
|
|
178
|
+
/** non-interactive 模式的可审计豁免理由。 */
|
|
179
|
+
nonInteractiveJustification?: string;
|
|
180
|
+
/** 应用声明的可恢复流程;restart 会触发 recovery contract 基线。 */
|
|
181
|
+
flows?: {
|
|
182
|
+
restart?: boolean;
|
|
183
|
+
reset?: boolean;
|
|
184
|
+
};
|
|
174
185
|
/** 生产 Scene registry 中预期出现的全部 Scene key。 */
|
|
175
186
|
scenes: string[];
|
|
176
187
|
/** 必须定义、执行并满足证据要求的玩法契约。 */
|
|
@@ -178,6 +189,8 @@ interface GameplayAuditOptions {
|
|
|
178
189
|
}
|
|
179
190
|
/** 通过 Vitest task metadata 在 worker 与 reporter 之间传递的契约数据。 */
|
|
180
191
|
interface GameplayContractMetadata {
|
|
192
|
+
/** 契约用途。 */
|
|
193
|
+
kind: GameplayAuditContract["kind"];
|
|
181
194
|
/** 契约唯一标识。 */
|
|
182
195
|
id: string;
|
|
183
196
|
/** 契约负责证明的生产 Scene key。 */
|
|
@@ -162,6 +162,8 @@ declare function createHeadlessGame<TScene extends Phaser.Scene>(scene: TScene,
|
|
|
162
162
|
|
|
163
163
|
/** 一个必须由指定测试文件证明的玩法契约。 */
|
|
164
164
|
interface GameplayAuditContract extends HeadlessGameplayEvidenceRequirements {
|
|
165
|
+
/** 契约用途;boot 不得替代交互游戏的 playthrough。 */
|
|
166
|
+
kind: "boot" | "playthrough" | "transition" | "recovery";
|
|
165
167
|
/** 在整个游戏项目中唯一且稳定的契约标识。 */
|
|
166
168
|
id: string;
|
|
167
169
|
/** 相对于游戏项目根目录的 Vitest 测试文件。 */
|
|
@@ -171,6 +173,15 @@ interface GameplayAuditContract extends HeadlessGameplayEvidenceRequirements {
|
|
|
171
173
|
}
|
|
172
174
|
/** 游戏项目在一次 Vitest 运行中必须完成的玩法审计清单。 */
|
|
173
175
|
interface GameplayAuditOptions {
|
|
176
|
+
/** 默认按交互游戏审计;纯演示项目必须显式豁免并说明原因。 */
|
|
177
|
+
mode?: "interactive" | "non-interactive";
|
|
178
|
+
/** non-interactive 模式的可审计豁免理由。 */
|
|
179
|
+
nonInteractiveJustification?: string;
|
|
180
|
+
/** 应用声明的可恢复流程;restart 会触发 recovery contract 基线。 */
|
|
181
|
+
flows?: {
|
|
182
|
+
restart?: boolean;
|
|
183
|
+
reset?: boolean;
|
|
184
|
+
};
|
|
174
185
|
/** 生产 Scene registry 中预期出现的全部 Scene key。 */
|
|
175
186
|
scenes: string[];
|
|
176
187
|
/** 必须定义、执行并满足证据要求的玩法契约。 */
|
|
@@ -178,6 +189,8 @@ interface GameplayAuditOptions {
|
|
|
178
189
|
}
|
|
179
190
|
/** 通过 Vitest task metadata 在 worker 与 reporter 之间传递的契约数据。 */
|
|
180
191
|
interface GameplayContractMetadata {
|
|
192
|
+
/** 契约用途。 */
|
|
193
|
+
kind: GameplayAuditContract["kind"];
|
|
181
194
|
/** 契约唯一标识。 */
|
|
182
195
|
id: string;
|
|
183
196
|
/** 契约负责证明的生产 Scene key。 */
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-
|
|
2
|
-
export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-
|
|
1
|
+
import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-XBGq_jIV.mjs';
|
|
2
|
+
export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-XBGq_jIV.mjs';
|
|
3
3
|
import * as Phaser from 'phaser';
|
|
4
4
|
import { TestContext, TestOptions } from 'vitest';
|
|
5
5
|
|
|
6
6
|
/** 将单个 Vitest 测试绑定到玩法 Ledger 的契约选项。 */
|
|
7
7
|
interface GameplayContractOptions extends HeadlessGameplayEvidenceRequirements {
|
|
8
|
+
/** 契约用途;交互项目不能只使用 boot。 */
|
|
9
|
+
kind: "boot" | "playthrough" | "transition" | "recovery";
|
|
8
10
|
/** 生产玩法清单中稳定且唯一的 contract 标识。 */
|
|
9
11
|
id: string;
|
|
10
12
|
/** 当前契约负责证明的生产 Scene key。 */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-
|
|
2
|
-
export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-
|
|
1
|
+
import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-XBGq_jIV.js';
|
|
2
|
+
export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-XBGq_jIV.js';
|
|
3
3
|
import * as Phaser from 'phaser';
|
|
4
4
|
import { TestContext, TestOptions } from 'vitest';
|
|
5
5
|
|
|
6
6
|
/** 将单个 Vitest 测试绑定到玩法 Ledger 的契约选项。 */
|
|
7
7
|
interface GameplayContractOptions extends HeadlessGameplayEvidenceRequirements {
|
|
8
|
+
/** 契约用途;交互项目不能只使用 boot。 */
|
|
9
|
+
kind: "boot" | "playthrough" | "transition" | "recovery";
|
|
8
10
|
/** 生产玩法清单中稳定且唯一的 contract 标识。 */
|
|
9
11
|
id: string;
|
|
10
12
|
/** 当前契约负责证明的生产 Scene key。 */
|
package/dist/index.js
CHANGED
|
@@ -1089,6 +1089,8 @@ async function createHeadlessGame(scene, options = {}) {
|
|
|
1089
1089
|
var import_vitest = require("vitest");
|
|
1090
1090
|
|
|
1091
1091
|
// src/gameplay-audit.ts
|
|
1092
|
+
var import_node_fs = require("fs");
|
|
1093
|
+
var import_node_path = require("path");
|
|
1092
1094
|
function normalizeList(value) {
|
|
1093
1095
|
if (value === void 0) return [];
|
|
1094
1096
|
const values = typeof value === "string" ? [value] : value;
|
|
@@ -1119,6 +1121,7 @@ function createGameplayContractMetadata(contract) {
|
|
|
1119
1121
|
const scenes = normalizeSet(contract.scenes);
|
|
1120
1122
|
return {
|
|
1121
1123
|
id: contract.id.trim(),
|
|
1124
|
+
kind: contract.kind,
|
|
1122
1125
|
scenes,
|
|
1123
1126
|
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
1124
1127
|
verified: false
|
package/dist/index.mjs
CHANGED
|
@@ -1047,6 +1047,8 @@ async function createHeadlessGame(scene, options = {}) {
|
|
|
1047
1047
|
import { test } from "vitest";
|
|
1048
1048
|
|
|
1049
1049
|
// src/gameplay-audit.ts
|
|
1050
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
1051
|
+
import { join } from "path";
|
|
1050
1052
|
function normalizeList(value) {
|
|
1051
1053
|
if (value === void 0) return [];
|
|
1052
1054
|
const values = typeof value === "string" ? [value] : value;
|
|
@@ -1077,6 +1079,7 @@ function createGameplayContractMetadata(contract) {
|
|
|
1077
1079
|
const scenes = normalizeSet(contract.scenes);
|
|
1078
1080
|
return {
|
|
1079
1081
|
id: contract.id.trim(),
|
|
1082
|
+
kind: contract.kind,
|
|
1080
1083
|
scenes,
|
|
1081
1084
|
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
1082
1085
|
verified: false
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
// src/gameplay-audit.ts
|
|
5
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
5
7
|
function normalizeList(value) {
|
|
6
8
|
if (value === void 0) return [];
|
|
7
9
|
const values = typeof value === "string" ? [value] : value;
|
|
@@ -35,20 +37,35 @@ function createGameplayContractMetadata(contract) {
|
|
|
35
37
|
const scenes = normalizeSet(contract.scenes);
|
|
36
38
|
return {
|
|
37
39
|
id: contract.id.trim(),
|
|
40
|
+
kind: contract.kind,
|
|
38
41
|
scenes,
|
|
39
42
|
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
40
43
|
verified: false
|
|
41
44
|
};
|
|
42
45
|
}
|
|
43
|
-
function validateGameplayAuditOptions(options2) {
|
|
46
|
+
function validateGameplayAuditOptions(options2, projectRoot) {
|
|
44
47
|
const issues = [];
|
|
45
|
-
const scenes =
|
|
48
|
+
const scenes = [...new Set(options2.scenes.map((scene) => scene.trim()).filter(Boolean))];
|
|
49
|
+
const mode = options2.mode ?? "interactive";
|
|
50
|
+
if (mode === "non-interactive") {
|
|
51
|
+
const justification = options2.nonInteractiveJustification?.trim() ?? "";
|
|
52
|
+
if (justification.length < 20) {
|
|
53
|
+
issues.push(
|
|
54
|
+
"non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (mode !== "interactive" && mode !== "non-interactive") {
|
|
59
|
+
issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
|
|
60
|
+
}
|
|
46
61
|
if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
47
62
|
if (scenes.length !== options2.scenes.length) {
|
|
48
63
|
issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
|
|
49
64
|
}
|
|
50
65
|
const contractIds = /* @__PURE__ */ new Set();
|
|
51
66
|
const coveredScenes = /* @__PURE__ */ new Set();
|
|
67
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
68
|
+
const invalidKindContracts = [];
|
|
52
69
|
for (const contract of options2.contracts) {
|
|
53
70
|
const id = contract.id.trim();
|
|
54
71
|
if (!id) {
|
|
@@ -67,6 +84,27 @@ function validateGameplayAuditOptions(options2) {
|
|
|
67
84
|
);
|
|
68
85
|
}
|
|
69
86
|
const contractScenes = normalizeSet(contract.scenes);
|
|
87
|
+
if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
|
|
88
|
+
invalidKindContracts.push(id || "<empty>");
|
|
89
|
+
} else {
|
|
90
|
+
const list = kinds.get(contract.kind) ?? [];
|
|
91
|
+
list.push(contract);
|
|
92
|
+
kinds.set(contract.kind, list);
|
|
93
|
+
}
|
|
94
|
+
if (contract.kind === "playthrough") {
|
|
95
|
+
if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
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.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
104
|
+
if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
105
|
+
if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
|
|
106
|
+
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`);
|
|
107
|
+
}
|
|
70
108
|
if (contractScenes.length === 0) {
|
|
71
109
|
issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
|
|
72
110
|
}
|
|
@@ -79,6 +117,11 @@ function validateGameplayAuditOptions(options2) {
|
|
|
79
117
|
}
|
|
80
118
|
}
|
|
81
119
|
}
|
|
120
|
+
if (invalidKindContracts.length > 0) {
|
|
121
|
+
issues.push(
|
|
122
|
+
`\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`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
82
125
|
for (const scene of scenes) {
|
|
83
126
|
if (!coveredScenes.has(scene)) {
|
|
84
127
|
issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
|
|
@@ -86,11 +129,71 @@ function validateGameplayAuditOptions(options2) {
|
|
|
86
129
|
}
|
|
87
130
|
if (options2.contracts.length === 0)
|
|
88
131
|
issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
132
|
+
if (mode === "interactive") {
|
|
133
|
+
const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
|
|
134
|
+
const hasInput = options2.contracts.some((contract) => contract.requireInput);
|
|
135
|
+
const hasCheckpoint = options2.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
|
|
136
|
+
if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
|
|
137
|
+
issues.push(
|
|
138
|
+
'\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'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const entryScene = scenes[0];
|
|
142
|
+
for (const scene of scenes.slice(1)) {
|
|
143
|
+
const transitioned = options2.contracts.some(
|
|
144
|
+
(contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
|
|
145
|
+
);
|
|
146
|
+
if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
|
|
147
|
+
}
|
|
148
|
+
if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
|
|
149
|
+
issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
|
|
150
|
+
}
|
|
151
|
+
const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
|
|
152
|
+
if (restartSignal && options2.flows?.restart !== true) {
|
|
153
|
+
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");
|
|
154
|
+
} else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
|
|
155
|
+
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
156
|
+
} else if ((options2.flows?.restart || options2.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
|
|
157
|
+
issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
89
160
|
return issues;
|
|
90
161
|
}
|
|
162
|
+
function containsProductionRestart(projectRoot) {
|
|
163
|
+
const sceneRoot = join(projectRoot, "src", "scenes");
|
|
164
|
+
const visit = (directory) => {
|
|
165
|
+
let entries;
|
|
166
|
+
try {
|
|
167
|
+
entries = readdirSync(directory);
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
const path = join(directory, entry);
|
|
173
|
+
let stats;
|
|
174
|
+
try {
|
|
175
|
+
stats = statSync(path);
|
|
176
|
+
} catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (stats.isDirectory() && visit(path)) return true;
|
|
180
|
+
if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
|
|
181
|
+
try {
|
|
182
|
+
const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
183
|
+
if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
189
|
+
return visit(sceneRoot);
|
|
190
|
+
}
|
|
91
191
|
function getDefinitionIssues(expected, actual) {
|
|
92
192
|
const issues = [];
|
|
93
193
|
const expectedMetadata = createGameplayContractMetadata(expected);
|
|
194
|
+
if (expectedMetadata.kind !== actual.kind) {
|
|
195
|
+
issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
|
|
196
|
+
}
|
|
94
197
|
if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
|
|
95
198
|
issues.push(
|
|
96
199
|
`contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
@@ -242,10 +345,12 @@ function auditGameplayRun(options2, tests) {
|
|
|
242
345
|
|
|
243
346
|
// src/lint/gameplay-audit.test.ts
|
|
244
347
|
var options = {
|
|
348
|
+
mode: "interactive",
|
|
245
349
|
scenes: ["IndexScene", "BattleScene"],
|
|
246
350
|
contracts: [
|
|
247
351
|
{
|
|
248
352
|
id: "gameplay.main-flow",
|
|
353
|
+
kind: "playthrough",
|
|
249
354
|
testFile: "tests/gameplay.test.ts",
|
|
250
355
|
scenes: ["IndexScene", "BattleScene"],
|
|
251
356
|
requireInput: true,
|
|
@@ -284,6 +389,123 @@ function createPassingTest() {
|
|
|
284
389
|
};
|
|
285
390
|
}
|
|
286
391
|
describe("gameplay audit", () => {
|
|
392
|
+
it("\u62D2\u7EDD\u4EA4\u4E92\u9879\u76EE\u53EA\u6709 boot contract", () => {
|
|
393
|
+
const result = validateGameplayAuditOptions({
|
|
394
|
+
mode: "interactive",
|
|
395
|
+
scenes: ["IndexScene"],
|
|
396
|
+
contracts: [
|
|
397
|
+
{
|
|
398
|
+
id: "IndexScene.boot",
|
|
399
|
+
kind: "boot",
|
|
400
|
+
testFile: "tests/gameplay.test.ts",
|
|
401
|
+
scenes: ["IndexScene"]
|
|
402
|
+
}
|
|
403
|
+
]
|
|
404
|
+
});
|
|
405
|
+
expect(result).toContain(
|
|
406
|
+
'\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'
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
it("\u5141\u8BB8\u5E26\u7406\u7531\u7684\u975E\u4EA4\u4E92 boot-only \u9879\u76EE", () => {
|
|
410
|
+
expect(
|
|
411
|
+
validateGameplayAuditOptions({
|
|
412
|
+
mode: "non-interactive",
|
|
413
|
+
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",
|
|
414
|
+
scenes: ["IndexScene"],
|
|
415
|
+
contracts: [
|
|
416
|
+
{
|
|
417
|
+
id: "IndexScene.boot",
|
|
418
|
+
kind: "boot",
|
|
419
|
+
testFile: "tests/gameplay.test.ts",
|
|
420
|
+
scenes: ["IndexScene"]
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
})
|
|
424
|
+
).toEqual([]);
|
|
425
|
+
});
|
|
426
|
+
it("\u5F3A\u5236 recovery contract \u540C\u65F6\u58F0\u660E restart \u548C\u4E24\u4E2A checkpoint", () => {
|
|
427
|
+
const result = validateGameplayAuditOptions({
|
|
428
|
+
mode: "non-interactive",
|
|
429
|
+
nonInteractiveJustification: "\u6D4B\u8BD5 recovery contract \u7684\u914D\u7F6E\u6821\u9A8C\uFF0C\u4E0D\u4EE3\u8868\u771F\u5B9E\u4EA7\u54C1\u8C41\u514D\u3002",
|
|
430
|
+
scenes: ["BattleScene"],
|
|
431
|
+
contracts: [{
|
|
432
|
+
id: "BattleScene.recovery",
|
|
433
|
+
kind: "recovery",
|
|
434
|
+
testFile: "tests/gameplay.test.ts",
|
|
435
|
+
scenes: ["BattleScene"],
|
|
436
|
+
requireInput: true,
|
|
437
|
+
requireFrameAdvance: true
|
|
438
|
+
}]
|
|
439
|
+
});
|
|
440
|
+
expect(result).toEqual(expect.arrayContaining([
|
|
441
|
+
'recovery contract "BattleScene.recovery" \u5FC5\u987B\u58F0\u660E requireRestart\u3002',
|
|
442
|
+
'recovery contract "BattleScene.recovery" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002'
|
|
443
|
+
]));
|
|
444
|
+
});
|
|
445
|
+
it("\u4EA4\u4E92\u9879\u76EE\u8981\u6C42\u6BCF\u4E2A\u975E\u5165\u53E3 Scene \u6709\u751F\u4EA7\u8F6C\u573A\u8BC1\u636E\u58F0\u660E", () => {
|
|
446
|
+
const result = validateGameplayAuditOptions({
|
|
447
|
+
mode: "interactive",
|
|
448
|
+
scenes: ["MenuScene", "BattleScene"],
|
|
449
|
+
contracts: [{
|
|
450
|
+
id: "MenuScene.playthrough",
|
|
451
|
+
kind: "playthrough",
|
|
452
|
+
testFile: "tests/gameplay.test.ts",
|
|
453
|
+
scenes: ["MenuScene"],
|
|
454
|
+
requireInput: true,
|
|
455
|
+
requireFrameAdvance: true,
|
|
456
|
+
requireCheckpoint: "progress"
|
|
457
|
+
}]
|
|
458
|
+
});
|
|
459
|
+
expect(result).toContain('\u975E\u5165\u53E3 Scene "BattleScene" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002');
|
|
460
|
+
});
|
|
461
|
+
it("\u4E0D\u63A5\u53D7 boot contract \u4F2A\u88C5\u6210\u751F\u4EA7\u8F6C\u573A", () => {
|
|
462
|
+
const result = validateGameplayAuditOptions({
|
|
463
|
+
mode: "interactive",
|
|
464
|
+
scenes: ["MenuScene", "BattleScene"],
|
|
465
|
+
contracts: [
|
|
466
|
+
{
|
|
467
|
+
id: "MenuScene.playthrough",
|
|
468
|
+
kind: "playthrough",
|
|
469
|
+
testFile: "tests/gameplay.test.ts",
|
|
470
|
+
scenes: ["MenuScene"],
|
|
471
|
+
requireInput: true,
|
|
472
|
+
requireFrameAdvance: true,
|
|
473
|
+
requireCheckpoint: "progress"
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
id: "BattleScene.boot",
|
|
477
|
+
kind: "boot",
|
|
478
|
+
testFile: "tests/gameplay.test.ts",
|
|
479
|
+
scenes: ["BattleScene"],
|
|
480
|
+
requireTransition: "BattleScene"
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
});
|
|
484
|
+
expect(result).toContain(
|
|
485
|
+
'\u975E\u5165\u53E3 Scene "BattleScene" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002'
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
it("\u58F0\u660E restart flow \u540E\u4ECD\u5F3A\u5236 recovery contract", () => {
|
|
489
|
+
const result = validateGameplayAuditOptions({
|
|
490
|
+
mode: "interactive",
|
|
491
|
+
flows: { restart: true },
|
|
492
|
+
scenes: ["IndexScene"],
|
|
493
|
+
contracts: [
|
|
494
|
+
{
|
|
495
|
+
id: "IndexScene.playthrough",
|
|
496
|
+
kind: "playthrough",
|
|
497
|
+
testFile: "tests/gameplay.test.ts",
|
|
498
|
+
scenes: ["IndexScene"],
|
|
499
|
+
requireInput: true,
|
|
500
|
+
requireFrameAdvance: true,
|
|
501
|
+
requireCheckpoint: "progress"
|
|
502
|
+
}
|
|
503
|
+
]
|
|
504
|
+
});
|
|
505
|
+
expect(result).toContain(
|
|
506
|
+
"\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002"
|
|
507
|
+
);
|
|
508
|
+
});
|
|
287
509
|
it("\u62D2\u7EDD\u672A\u88AB contract \u8986\u76D6\u7684\u751F\u4EA7 Scene", () => {
|
|
288
510
|
expect(
|
|
289
511
|
validateGameplayAuditOptions({
|
|
@@ -299,6 +521,7 @@ describe("gameplay audit", () => {
|
|
|
299
521
|
contracts: [
|
|
300
522
|
{
|
|
301
523
|
id: " IndexScene.boot ",
|
|
524
|
+
kind: "boot",
|
|
302
525
|
testFile: "tests/gameplay.test.ts",
|
|
303
526
|
scenes: ["IndexScene"]
|
|
304
527
|
}
|
|
@@ -312,6 +535,7 @@ describe("gameplay audit", () => {
|
|
|
312
535
|
contracts: [
|
|
313
536
|
{
|
|
314
537
|
id: "IndexScene.boot",
|
|
538
|
+
kind: "boot",
|
|
315
539
|
testFile,
|
|
316
540
|
scenes: ["IndexScene"]
|
|
317
541
|
}
|
|
@@ -1051,6 +1051,8 @@ async function createHeadlessGame(scene, options = {}) {
|
|
|
1051
1051
|
import { test } from "vitest";
|
|
1052
1052
|
|
|
1053
1053
|
// src/gameplay-audit.ts
|
|
1054
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
1055
|
+
import { join } from "path";
|
|
1054
1056
|
function normalizeList(value) {
|
|
1055
1057
|
if (value === void 0) return [];
|
|
1056
1058
|
const values = typeof value === "string" ? [value] : value;
|
|
@@ -1081,6 +1083,7 @@ function createGameplayContractMetadata(contract) {
|
|
|
1081
1083
|
const scenes = normalizeSet(contract.scenes);
|
|
1082
1084
|
return {
|
|
1083
1085
|
id: contract.id.trim(),
|
|
1086
|
+
kind: contract.kind,
|
|
1084
1087
|
scenes,
|
|
1085
1088
|
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
1086
1089
|
verified: false
|
|
@@ -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`
|
|
@@ -330,7 +433,7 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts
|
|
|
330
433
|
|
|
331
434
|
// src/vitest-config.ts
|
|
332
435
|
function defineGameVitestConfig(options) {
|
|
333
|
-
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
|
|
436
|
+
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
|
|
334
437
|
if (auditIssues.length > 0) {
|
|
335
438
|
throw new Error(
|
|
336
439
|
`\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
|
|
@@ -400,10 +503,13 @@ function defineGameVitestConfig(options) {
|
|
|
400
503
|
// src/lint/vitest-config.test.ts
|
|
401
504
|
var projectRoot = resolve2(import.meta.dirname, "../..");
|
|
402
505
|
var gameplayAudit = {
|
|
506
|
+
mode: "non-interactive",
|
|
507
|
+
nonInteractiveJustification: "\u56FA\u5B9A\u5939\u5177\u53EA\u9A8C\u8BC1\u5171\u4EAB host \u57FA\u7EBF\uFF0C\u4E0D\u5305\u542B\u73A9\u6CD5\u8F93\u5165\u3002",
|
|
403
508
|
scenes: ["InputScene"],
|
|
404
509
|
contracts: [
|
|
405
510
|
{
|
|
406
511
|
id: "InputScene.boot",
|
|
512
|
+
kind: "boot",
|
|
407
513
|
testFile: "tests/input.test.ts",
|
|
408
514
|
scenes: ["InputScene"],
|
|
409
515
|
requireFrameAdvance: true
|
|
@@ -476,4 +582,16 @@ describe("defineGameVitestConfig", () => {
|
|
|
476
582
|
})
|
|
477
583
|
).toThrow(/ResultScene.*没有任何 gameplay contract/s);
|
|
478
584
|
});
|
|
585
|
+
it("\u62D2\u7EDD\u4EA4\u4E92\u9879\u76EE\u53EA\u6709 boot smoke test", () => {
|
|
586
|
+
expect(
|
|
587
|
+
() => defineGameVitestConfig({
|
|
588
|
+
projectRoot,
|
|
589
|
+
gameplayAudit: {
|
|
590
|
+
mode: "interactive",
|
|
591
|
+
scenes: ["InputScene"],
|
|
592
|
+
contracts: gameplayAudit.contracts
|
|
593
|
+
}
|
|
594
|
+
})
|
|
595
|
+
).toThrow(/缺少最低玩法基线/);
|
|
596
|
+
});
|
|
479
597
|
});
|
package/dist/vitest-config.d.mts
CHANGED
package/dist/vitest-config.d.ts
CHANGED
package/dist/vitest-config.js
CHANGED
|
@@ -23,10 +23,12 @@ __export(vitest_config_exports, {
|
|
|
23
23
|
defineGameVitestConfig: () => defineGameVitestConfig
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(vitest_config_exports);
|
|
26
|
-
var
|
|
26
|
+
var import_node_path3 = require("path");
|
|
27
27
|
var import_config = require("vitest/config");
|
|
28
28
|
|
|
29
29
|
// src/gameplay-audit.ts
|
|
30
|
+
var import_node_fs = require("fs");
|
|
31
|
+
var import_node_path = require("path");
|
|
30
32
|
function normalizeList(value) {
|
|
31
33
|
if (value === void 0) return [];
|
|
32
34
|
const values = typeof value === "string" ? [value] : value;
|
|
@@ -60,20 +62,35 @@ function createGameplayContractMetadata(contract) {
|
|
|
60
62
|
const scenes = normalizeSet(contract.scenes);
|
|
61
63
|
return {
|
|
62
64
|
id: contract.id.trim(),
|
|
65
|
+
kind: contract.kind,
|
|
63
66
|
scenes,
|
|
64
67
|
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
65
68
|
verified: false
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
|
-
function validateGameplayAuditOptions(options) {
|
|
71
|
+
function validateGameplayAuditOptions(options, projectRoot) {
|
|
69
72
|
const issues = [];
|
|
70
|
-
const scenes =
|
|
73
|
+
const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
|
|
74
|
+
const mode = options.mode ?? "interactive";
|
|
75
|
+
if (mode === "non-interactive") {
|
|
76
|
+
const justification = options.nonInteractiveJustification?.trim() ?? "";
|
|
77
|
+
if (justification.length < 20) {
|
|
78
|
+
issues.push(
|
|
79
|
+
"non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (mode !== "interactive" && mode !== "non-interactive") {
|
|
84
|
+
issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
|
|
85
|
+
}
|
|
71
86
|
if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
72
87
|
if (scenes.length !== options.scenes.length) {
|
|
73
88
|
issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
|
|
74
89
|
}
|
|
75
90
|
const contractIds = /* @__PURE__ */ new Set();
|
|
76
91
|
const coveredScenes = /* @__PURE__ */ new Set();
|
|
92
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
93
|
+
const invalidKindContracts = [];
|
|
77
94
|
for (const contract of options.contracts) {
|
|
78
95
|
const id = contract.id.trim();
|
|
79
96
|
if (!id) {
|
|
@@ -92,6 +109,27 @@ function validateGameplayAuditOptions(options) {
|
|
|
92
109
|
);
|
|
93
110
|
}
|
|
94
111
|
const contractScenes = normalizeSet(contract.scenes);
|
|
112
|
+
if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
|
|
113
|
+
invalidKindContracts.push(id || "<empty>");
|
|
114
|
+
} else {
|
|
115
|
+
const list = kinds.get(contract.kind) ?? [];
|
|
116
|
+
list.push(contract);
|
|
117
|
+
kinds.set(contract.kind, list);
|
|
118
|
+
}
|
|
119
|
+
if (contract.kind === "playthrough") {
|
|
120
|
+
if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
121
|
+
if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
122
|
+
if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
|
|
123
|
+
}
|
|
124
|
+
if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
|
|
125
|
+
issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
|
|
126
|
+
}
|
|
127
|
+
if (contract.kind === "recovery") {
|
|
128
|
+
if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
129
|
+
if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
130
|
+
if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
|
|
131
|
+
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`);
|
|
132
|
+
}
|
|
95
133
|
if (contractScenes.length === 0) {
|
|
96
134
|
issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
|
|
97
135
|
}
|
|
@@ -104,6 +142,11 @@ function validateGameplayAuditOptions(options) {
|
|
|
104
142
|
}
|
|
105
143
|
}
|
|
106
144
|
}
|
|
145
|
+
if (invalidKindContracts.length > 0) {
|
|
146
|
+
issues.push(
|
|
147
|
+
`\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`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
107
150
|
for (const scene of scenes) {
|
|
108
151
|
if (!coveredScenes.has(scene)) {
|
|
109
152
|
issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
|
|
@@ -111,11 +154,71 @@ function validateGameplayAuditOptions(options) {
|
|
|
111
154
|
}
|
|
112
155
|
if (options.contracts.length === 0)
|
|
113
156
|
issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
157
|
+
if (mode === "interactive") {
|
|
158
|
+
const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
|
|
159
|
+
const hasInput = options.contracts.some((contract) => contract.requireInput);
|
|
160
|
+
const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
|
|
161
|
+
if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
|
|
162
|
+
issues.push(
|
|
163
|
+
'\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'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const entryScene = scenes[0];
|
|
167
|
+
for (const scene of scenes.slice(1)) {
|
|
168
|
+
const transitioned = options.contracts.some(
|
|
169
|
+
(contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
|
|
170
|
+
);
|
|
171
|
+
if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
|
|
172
|
+
}
|
|
173
|
+
if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
|
|
174
|
+
issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
|
|
175
|
+
}
|
|
176
|
+
const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
|
|
177
|
+
if (restartSignal && options.flows?.restart !== true) {
|
|
178
|
+
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");
|
|
179
|
+
} else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
|
|
180
|
+
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
181
|
+
} else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
|
|
182
|
+
issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
114
185
|
return issues;
|
|
115
186
|
}
|
|
187
|
+
function containsProductionRestart(projectRoot) {
|
|
188
|
+
const sceneRoot = (0, import_node_path.join)(projectRoot, "src", "scenes");
|
|
189
|
+
const visit = (directory) => {
|
|
190
|
+
let entries;
|
|
191
|
+
try {
|
|
192
|
+
entries = (0, import_node_fs.readdirSync)(directory);
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
const path = (0, import_node_path.join)(directory, entry);
|
|
198
|
+
let stats;
|
|
199
|
+
try {
|
|
200
|
+
stats = (0, import_node_fs.statSync)(path);
|
|
201
|
+
} catch {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (stats.isDirectory() && visit(path)) return true;
|
|
205
|
+
if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
|
|
206
|
+
try {
|
|
207
|
+
const source = (0, import_node_fs.readFileSync)(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
208
|
+
if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
};
|
|
214
|
+
return visit(sceneRoot);
|
|
215
|
+
}
|
|
116
216
|
function getDefinitionIssues(expected, actual) {
|
|
117
217
|
const issues = [];
|
|
118
218
|
const expectedMetadata = createGameplayContractMetadata(expected);
|
|
219
|
+
if (expectedMetadata.kind !== actual.kind) {
|
|
220
|
+
issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
|
|
221
|
+
}
|
|
119
222
|
if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
|
|
120
223
|
issues.push(
|
|
121
224
|
`contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
@@ -266,7 +369,7 @@ function auditGameplayRun(options, tests) {
|
|
|
266
369
|
}
|
|
267
370
|
|
|
268
371
|
// src/gameplay-audit-reporter.ts
|
|
269
|
-
var
|
|
372
|
+
var import_node_path2 = require("path");
|
|
270
373
|
function normalizePath(path) {
|
|
271
374
|
return path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
272
375
|
}
|
|
@@ -304,7 +407,7 @@ var GameplayAuditReporter = class {
|
|
|
304
407
|
this.preflightIssues.clear();
|
|
305
408
|
const scheduledFiles = new Set(
|
|
306
409
|
specifications.map(
|
|
307
|
-
(specification) => normalizePath((0,
|
|
410
|
+
(specification) => normalizePath((0, import_node_path2.relative)(this.projectRoot, specification.moduleId))
|
|
308
411
|
)
|
|
309
412
|
);
|
|
310
413
|
for (const contract of this.options.contracts) {
|
|
@@ -350,7 +453,7 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts
|
|
|
350
453
|
|
|
351
454
|
// src/vitest-config.ts
|
|
352
455
|
function defineGameVitestConfig(options) {
|
|
353
|
-
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
|
|
456
|
+
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
|
|
354
457
|
if (auditIssues.length > 0) {
|
|
355
458
|
throw new Error(
|
|
356
459
|
`\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
|
|
@@ -361,7 +464,7 @@ function defineGameVitestConfig(options) {
|
|
|
361
464
|
resolve: {
|
|
362
465
|
alias: {
|
|
363
466
|
...options.aliases,
|
|
364
|
-
"@": (0,
|
|
467
|
+
"@": (0, import_node_path3.resolve)(options.projectRoot, "src")
|
|
365
468
|
}
|
|
366
469
|
},
|
|
367
470
|
// devkit 作为 external ESM 加载时,应用测试也必须 externalize Phaser,
|
package/dist/vitest-config.mjs
CHANGED
|
@@ -3,6 +3,8 @@ import { resolve } from "path";
|
|
|
3
3
|
import { defineConfig } from "vitest/config";
|
|
4
4
|
|
|
5
5
|
// src/gameplay-audit.ts
|
|
6
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
6
8
|
function normalizeList(value) {
|
|
7
9
|
if (value === void 0) return [];
|
|
8
10
|
const values = typeof value === "string" ? [value] : value;
|
|
@@ -36,20 +38,35 @@ function createGameplayContractMetadata(contract) {
|
|
|
36
38
|
const scenes = normalizeSet(contract.scenes);
|
|
37
39
|
return {
|
|
38
40
|
id: contract.id.trim(),
|
|
41
|
+
kind: contract.kind,
|
|
39
42
|
scenes,
|
|
40
43
|
requirements: normalizeGameplayRequirements(contract, scenes),
|
|
41
44
|
verified: false
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
|
-
function validateGameplayAuditOptions(options) {
|
|
47
|
+
function validateGameplayAuditOptions(options, projectRoot) {
|
|
45
48
|
const issues = [];
|
|
46
|
-
const scenes =
|
|
49
|
+
const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
|
|
50
|
+
const mode = options.mode ?? "interactive";
|
|
51
|
+
if (mode === "non-interactive") {
|
|
52
|
+
const justification = options.nonInteractiveJustification?.trim() ?? "";
|
|
53
|
+
if (justification.length < 20) {
|
|
54
|
+
issues.push(
|
|
55
|
+
"non-interactive \u6A21\u5F0F\u5FC5\u987B\u63D0\u4F9B\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684 nonInteractiveJustification\u3002"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (mode !== "interactive" && mode !== "non-interactive") {
|
|
60
|
+
issues.push("gameplayAudit.mode \u5FC5\u987B\u662F interactive \u6216 non-interactive\u3002");
|
|
61
|
+
}
|
|
47
62
|
if (scenes.length === 0) issues.push("gameplayAudit.scenes \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
48
63
|
if (scenes.length !== options.scenes.length) {
|
|
49
64
|
issues.push("gameplayAudit.scenes \u4E0D\u80FD\u5305\u542B\u7A7A\u503C\u6216\u91CD\u590D Scene key\u3002");
|
|
50
65
|
}
|
|
51
66
|
const contractIds = /* @__PURE__ */ new Set();
|
|
52
67
|
const coveredScenes = /* @__PURE__ */ new Set();
|
|
68
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
69
|
+
const invalidKindContracts = [];
|
|
53
70
|
for (const contract of options.contracts) {
|
|
54
71
|
const id = contract.id.trim();
|
|
55
72
|
if (!id) {
|
|
@@ -68,6 +85,27 @@ function validateGameplayAuditOptions(options) {
|
|
|
68
85
|
);
|
|
69
86
|
}
|
|
70
87
|
const contractScenes = normalizeSet(contract.scenes);
|
|
88
|
+
if (!["boot", "playthrough", "transition", "recovery"].includes(contract.kind)) {
|
|
89
|
+
invalidKindContracts.push(id || "<empty>");
|
|
90
|
+
} else {
|
|
91
|
+
const list = kinds.get(contract.kind) ?? [];
|
|
92
|
+
list.push(contract);
|
|
93
|
+
kinds.set(contract.kind, list);
|
|
94
|
+
}
|
|
95
|
+
if (contract.kind === "playthrough") {
|
|
96
|
+
if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
97
|
+
if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
98
|
+
if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
|
|
99
|
+
}
|
|
100
|
+
if (contract.kind === "transition" && normalizeList(contract.requireTransition).length === 0) {
|
|
101
|
+
issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
|
|
102
|
+
}
|
|
103
|
+
if (contract.kind === "recovery") {
|
|
104
|
+
if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
|
|
105
|
+
if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
|
|
106
|
+
if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
|
|
107
|
+
if (normalizeList(contract.requireCheckpoint).length < 2) issues.push(`recovery contract "${id}" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002`);
|
|
108
|
+
}
|
|
71
109
|
if (contractScenes.length === 0) {
|
|
72
110
|
issues.push(`contract "${id || "<empty>"}" \u5FC5\u987B\u58F0\u660E\u81F3\u5C11\u4E00\u4E2A Scene\u3002`);
|
|
73
111
|
}
|
|
@@ -80,6 +118,11 @@ function validateGameplayAuditOptions(options) {
|
|
|
80
118
|
}
|
|
81
119
|
}
|
|
82
120
|
}
|
|
121
|
+
if (invalidKindContracts.length > 0) {
|
|
122
|
+
issues.push(
|
|
123
|
+
`\u4EE5\u4E0B contract \u7F3A\u5C11\u6709\u6548 kind\uFF1A${invalidKindContracts.map((id) => `"${id}"`).join("\u3001")}\u3002\u8BF7\u4E3A\u6BCF\u4E2A contract \u8BBE\u7F6E kind: "boot"\u3001"playthrough"\u3001"transition" \u6216 "recovery"\u3002`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
83
126
|
for (const scene of scenes) {
|
|
84
127
|
if (!coveredScenes.has(scene)) {
|
|
85
128
|
issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
|
|
@@ -87,11 +130,71 @@ function validateGameplayAuditOptions(options) {
|
|
|
87
130
|
}
|
|
88
131
|
if (options.contracts.length === 0)
|
|
89
132
|
issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
|
|
133
|
+
if (mode === "interactive") {
|
|
134
|
+
const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
|
|
135
|
+
const hasInput = options.contracts.some((contract) => contract.requireInput);
|
|
136
|
+
const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
|
|
137
|
+
if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
|
|
138
|
+
issues.push(
|
|
139
|
+
'\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A kind: "playthrough" contract \u5FC5\u987B\u540C\u65F6\u58F0\u660E requireInput: true\u3001requireFrameAdvance: true \u548C requireCheckpoint: ["progress"]\u3002'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const entryScene = scenes[0];
|
|
143
|
+
for (const scene of scenes.slice(1)) {
|
|
144
|
+
const transitioned = options.contracts.some(
|
|
145
|
+
(contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
|
|
146
|
+
);
|
|
147
|
+
if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
|
|
148
|
+
}
|
|
149
|
+
if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
|
|
150
|
+
issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
|
|
151
|
+
}
|
|
152
|
+
const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
|
|
153
|
+
if (restartSignal && options.flows?.restart !== true) {
|
|
154
|
+
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF1B\u5FC5\u987B\u58F0\u660E flows.restart: true \u5E76\u63D0\u4F9B kind: recovery contract\u3002");
|
|
155
|
+
} else if (restartSignal && (kinds.get("recovery") ?? []).length === 0) {
|
|
156
|
+
issues.push("\u68C0\u6D4B\u5230\u751F\u4EA7 Scene \u4F7F\u7528 scene.restart()\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
157
|
+
} else if ((options.flows?.restart || options.flows?.reset) && (kinds.get("recovery") ?? []).length === 0) {
|
|
158
|
+
issues.push("\u9879\u76EE\u58F0\u660E\u6216\u5B9E\u73B0\u4E86 restart/reset \u6D41\u7A0B\uFF0C\u4F46\u7F3A\u5C11 kind: recovery contract\u3002");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
90
161
|
return issues;
|
|
91
162
|
}
|
|
163
|
+
function containsProductionRestart(projectRoot) {
|
|
164
|
+
const sceneRoot = join(projectRoot, "src", "scenes");
|
|
165
|
+
const visit = (directory) => {
|
|
166
|
+
let entries;
|
|
167
|
+
try {
|
|
168
|
+
entries = readdirSync(directory);
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const path = join(directory, entry);
|
|
174
|
+
let stats;
|
|
175
|
+
try {
|
|
176
|
+
stats = statSync(path);
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (stats.isDirectory() && visit(path)) return true;
|
|
181
|
+
if (!stats.isFile() || !/\.(ts|tsx)$/.test(entry)) continue;
|
|
182
|
+
try {
|
|
183
|
+
const source = readFileSync(path, "utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|
|
184
|
+
if (/\bscene\s*\.\s*restart\s*\(/.test(source)) return true;
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
};
|
|
190
|
+
return visit(sceneRoot);
|
|
191
|
+
}
|
|
92
192
|
function getDefinitionIssues(expected, actual) {
|
|
93
193
|
const issues = [];
|
|
94
194
|
const expectedMetadata = createGameplayContractMetadata(expected);
|
|
195
|
+
if (expectedMetadata.kind !== actual.kind) {
|
|
196
|
+
issues.push(`contract "${expected.id}" \u7684 kind \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`);
|
|
197
|
+
}
|
|
95
198
|
if (!sameSet(expectedMetadata.scenes, actual.scenes)) {
|
|
96
199
|
issues.push(
|
|
97
200
|
`contract "${expected.id}" \u58F0\u660E\u7684 Scene \u4E0E gameplayAudit \u6E05\u5355\u4E0D\u4E00\u81F4\u3002`
|
|
@@ -326,7 +429,7 @@ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts
|
|
|
326
429
|
|
|
327
430
|
// src/vitest-config.ts
|
|
328
431
|
function defineGameVitestConfig(options) {
|
|
329
|
-
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit);
|
|
432
|
+
const auditIssues = validateGameplayAuditOptions(options.gameplayAudit, options.projectRoot);
|
|
330
433
|
if (auditIssues.length > 0) {
|
|
331
434
|
throw new Error(
|
|
332
435
|
`\u65E0\u6548\u7684 gameplayAudit \u914D\u7F6E\uFF1A
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miaoda-game-devkit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -43,6 +43,18 @@
|
|
|
43
43
|
"!dist/**/*.map",
|
|
44
44
|
"!dist/lint/**/*.test.*"
|
|
45
45
|
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"prepack": "pnpm run build",
|
|
49
|
+
"release": "pnpm run test && clean-publish",
|
|
50
|
+
"release:dry-run": "pnpm run test && clean-publish --dry-run",
|
|
51
|
+
"release:beta": "pnpm run test && npm version prerelease --preid=beta && clean-publish --tag beta",
|
|
52
|
+
"release:patch": "pnpm run test && npm version patch && clean-publish",
|
|
53
|
+
"release:minor": "pnpm run test && npm version minor && clean-publish",
|
|
54
|
+
"release:major": "pnpm run test && npm version major && clean-publish",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"test": "pnpm run build && ./bin/miaoda-game-lint.js --contracts-only"
|
|
57
|
+
},
|
|
46
58
|
"nx": {
|
|
47
59
|
"tags": [
|
|
48
60
|
"type:package",
|
|
@@ -82,8 +94,23 @@
|
|
|
82
94
|
"peerDependencies": {
|
|
83
95
|
"phaser": ">=4.0.0 <5"
|
|
84
96
|
},
|
|
97
|
+
"devDependencies": {
|
|
98
|
+
"@types/eslint": "^9.6.1",
|
|
99
|
+
"@types/estree": "^1.0.9",
|
|
100
|
+
"@types/node": "^24.13.3",
|
|
101
|
+
"clean-publish": "^6.0.5",
|
|
102
|
+
"phaser": "4.0.0",
|
|
103
|
+
"tsup": "^8.5.1",
|
|
104
|
+
"typescript": "^5.9.3"
|
|
105
|
+
},
|
|
85
106
|
"publishConfig": {
|
|
86
107
|
"registry": "https://registry.npmjs.org/",
|
|
87
108
|
"access": "public"
|
|
109
|
+
},
|
|
110
|
+
"clean-publish": {
|
|
111
|
+
"fields": [
|
|
112
|
+
"devDependencies",
|
|
113
|
+
"scripts"
|
|
114
|
+
]
|
|
88
115
|
}
|
|
89
116
|
}
|