@dreamboard-games/workspace-codegen 0.1.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.
Files changed (73) hide show
  1. package/LICENSE +89 -0
  2. package/NOTICE +1 -0
  3. package/dist/hex-geometry.d.ts +2 -0
  4. package/dist/hex-geometry.js +49 -0
  5. package/dist/index.d.ts +13 -0
  6. package/dist/index.js +22 -0
  7. package/dist/manifest-contract.d.ts +14 -0
  8. package/dist/manifest-contract.js +4897 -0
  9. package/dist/manifest-validation.d.ts +6 -0
  10. package/dist/manifest-validation.js +506 -0
  11. package/dist/ownership.d.ts +31 -0
  12. package/dist/ownership.js +86 -0
  13. package/dist/preset-card-sets.d.ts +5 -0
  14. package/dist/preset-card-sets.js +135 -0
  15. package/dist/seeds.d.ts +6 -0
  16. package/dist/seeds.js +766 -0
  17. package/ownership.json +51 -0
  18. package/package.json +46 -0
  19. package/src/__fixtures__/sdk-types/invalid-card-properties-extra-key.ts +62 -0
  20. package/src/__fixtures__/sdk-types/invalid-card-properties-missing-required.ts +60 -0
  21. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-enum.ts +61 -0
  22. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-nested.ts +61 -0
  23. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-scalar.ts +61 -0
  24. package/src/__fixtures__/sdk-types/invalid-card-visibility.ts +40 -0
  25. package/src/__fixtures__/sdk-types/invalid-container-card-set-manifest.ts +62 -0
  26. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-array-item.ts +43 -0
  27. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-player-id.ts +43 -0
  28. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-resource-id.ts +43 -0
  29. package/src/__fixtures__/sdk-types/invalid-die-home-per-player-zone-no-owner.ts +35 -0
  30. package/src/__fixtures__/sdk-types/invalid-die-seed-type-id.ts +31 -0
  31. package/src/__fixtures__/sdk-types/invalid-die-visibility.ts +28 -0
  32. package/src/__fixtures__/sdk-types/invalid-generic-board-edge-id.ts +38 -0
  33. package/src/__fixtures__/sdk-types/invalid-generic-board-nested-field.ts +45 -0
  34. package/src/__fixtures__/sdk-types/invalid-hex-edge-field-edge-id.ts +47 -0
  35. package/src/__fixtures__/sdk-types/invalid-hex-vertex-field-vertex-id.ts +47 -0
  36. package/src/__fixtures__/sdk-types/invalid-manifest.ts +143 -0
  37. package/src/__fixtures__/sdk-types/invalid-piece-fields-extra-key.ts +62 -0
  38. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-card-id.ts +61 -0
  39. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-enum.ts +61 -0
  40. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-scalar.ts +61 -0
  41. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-zone-id.ts +61 -0
  42. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-container-no-owner.ts +48 -0
  43. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-edge-no-owner.ts +47 -0
  44. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-space-no-owner.ts +42 -0
  45. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-vertex-no-owner.ts +49 -0
  46. package/src/__fixtures__/sdk-types/invalid-piece-seed-type-id.ts +30 -0
  47. package/src/__fixtures__/sdk-types/invalid-piece-visibility.ts +28 -0
  48. package/src/__fixtures__/sdk-types/invalid-slot-host-manifest.ts +47 -0
  49. package/src/__fixtures__/sdk-types/invalid-slot-id-manifest.ts +49 -0
  50. package/src/__fixtures__/sdk-types/invalid-square-board-edge-id.ts +47 -0
  51. package/src/__fixtures__/sdk-types/invalid-square-board-space-id.ts +47 -0
  52. package/src/__fixtures__/sdk-types/invalid-square-board-vertex-id.ts +49 -0
  53. package/src/__fixtures__/sdk-types/invalid-square-container-field-space-id.ts +59 -0
  54. package/src/__fixtures__/sdk-types/invalid-square-container-host-space-id.ts +49 -0
  55. package/src/__fixtures__/sdk-types/invalid-square-relation-field-scalar.ts +48 -0
  56. package/src/__fixtures__/sdk-types/invalid-square-relation-space-id.ts +48 -0
  57. package/src/__fixtures__/sdk-types/invalid-square-space-fields-enum.ts +44 -0
  58. package/src/__fixtures__/sdk-types/invalid-square-space-fields-extra-key.ts +45 -0
  59. package/src/__fixtures__/sdk-types/valid-die-type-omits-sides.ts +29 -0
  60. package/src/__fixtures__/sdk-types/valid-manifest-omits-board-templates.ts +19 -0
  61. package/src/__fixtures__/sdk-types/valid-manifest.ts +612 -0
  62. package/src/__fixtures__/sdk-types/valid-player-scoped-seed-homes.ts +59 -0
  63. package/src/authoring-benchmark.test.ts +362 -0
  64. package/src/hex-geometry.ts +69 -0
  65. package/src/index.ts +64 -0
  66. package/src/manifest-contract.test.ts +1764 -0
  67. package/src/manifest-contract.ts +6581 -0
  68. package/src/manifest-validation.test.ts +393 -0
  69. package/src/manifest-validation.ts +795 -0
  70. package/src/ownership.ts +127 -0
  71. package/src/preset-card-sets.ts +169 -0
  72. package/src/sdk-types-authoring.test.ts +361 -0
  73. package/src/seeds.ts +800 -0
@@ -0,0 +1,127 @@
1
+ export type OwnershipPattern = {
2
+ prefix: string;
3
+ suffix: string;
4
+ };
5
+
6
+ export type ScaffoldingOwnership = {
7
+ version: number;
8
+ allowedPaths: {
9
+ rootFiles: string[];
10
+ directoryPrefixes: string[];
11
+ };
12
+ dynamic: {
13
+ generatedFiles: string[];
14
+ seedFiles: string[];
15
+ seedFilePatterns: OwnershipPattern[];
16
+ };
17
+ cliStatic: {
18
+ exactFiles: string[];
19
+ directoryPrefixes: string[];
20
+ };
21
+ preservedUserFiles: string[];
22
+ };
23
+
24
+ export const WORKSPACE_CODEGEN_OWNERSHIP: ScaffoldingOwnership = {
25
+ version: 26,
26
+ allowedPaths: {
27
+ rootFiles: [
28
+ ".npmrc",
29
+ "package.json",
30
+ "pnpm-lock.yaml",
31
+ "package-lock.json",
32
+ "manifest.ts",
33
+ "manifest.tsconfig.json",
34
+ "rule.md",
35
+ ],
36
+ directoryPrefixes: ["app/", "ui/", "shared/", "test/"],
37
+ },
38
+ dynamic: {
39
+ generatedFiles: [
40
+ "shared/manifest-literals.ts",
41
+ "shared/manifest-types.ts",
42
+ "shared/manifest-runtime.ts",
43
+ "shared/manifest-contract.ts",
44
+ "shared/generated/ui-contract.ts",
45
+ "app/index.ts",
46
+ "app/tsconfig.framework.json",
47
+ "ui/tsconfig.framework.json",
48
+ ],
49
+ seedFiles: [
50
+ "app/README.md",
51
+ "ui/App.tsx",
52
+ "app/game-contract.ts",
53
+ "app/game.ts",
54
+ "app/setup-profiles.ts",
55
+ "app/reducer-support.ts",
56
+ "app/derived.ts",
57
+ ],
58
+ seedFilePatterns: [{ prefix: "app/phases/", suffix: ".ts" }],
59
+ },
60
+ cliStatic: {
61
+ exactFiles: [
62
+ ".npmrc",
63
+ "package.json",
64
+ "app/tsconfig.json",
65
+ "ui/index.tsx",
66
+ "ui/package.json",
67
+ "ui/style.css",
68
+ "ui/tsconfig.json",
69
+ ],
70
+ directoryPrefixes: [],
71
+ },
72
+ preservedUserFiles: [],
73
+ } as const;
74
+
75
+ export const AUTHORITATIVE_GENERATED_FILES =
76
+ WORKSPACE_CODEGEN_OWNERSHIP.dynamic.generatedFiles;
77
+ export const SEED_FILES = WORKSPACE_CODEGEN_OWNERSHIP.dynamic.seedFiles;
78
+ export const SEED_FILE_PATTERNS =
79
+ WORKSPACE_CODEGEN_OWNERSHIP.dynamic.seedFilePatterns;
80
+ export const PRESERVED_USER_FILES = new Set(
81
+ WORKSPACE_CODEGEN_OWNERSHIP.preservedUserFiles,
82
+ );
83
+
84
+ function normalizeProjectPath(filePath: string): string {
85
+ return filePath.replace(/^\.\//, "").replace(/^\/+/, "").replace(/\\/g, "/");
86
+ }
87
+
88
+ export function isAllowedGamePath(filePath: string): boolean {
89
+ const path = normalizeProjectPath(filePath);
90
+ if (WORKSPACE_CODEGEN_OWNERSHIP.allowedPaths.rootFiles.includes(path)) {
91
+ return true;
92
+ }
93
+ return WORKSPACE_CODEGEN_OWNERSHIP.allowedPaths.directoryPrefixes.some(
94
+ (prefix) => path.startsWith(prefix),
95
+ );
96
+ }
97
+
98
+ export function isAuthoritativeGeneratedPath(filePath: string): boolean {
99
+ const path = normalizeProjectPath(filePath);
100
+ return WORKSPACE_CODEGEN_OWNERSHIP.dynamic.generatedFiles.includes(path);
101
+ }
102
+
103
+ export function isDynamicSeedPath(filePath: string): boolean {
104
+ const path = normalizeProjectPath(filePath);
105
+ if (WORKSPACE_CODEGEN_OWNERSHIP.dynamic.seedFiles.includes(path)) {
106
+ return true;
107
+ }
108
+ return WORKSPACE_CODEGEN_OWNERSHIP.dynamic.seedFilePatterns.some(
109
+ (pattern) =>
110
+ path.startsWith(pattern.prefix) && path.endsWith(pattern.suffix),
111
+ );
112
+ }
113
+
114
+ export function isCliStaticPath(filePath: string): boolean {
115
+ const path = normalizeProjectPath(filePath);
116
+ if (WORKSPACE_CODEGEN_OWNERSHIP.cliStatic.exactFiles.includes(path)) {
117
+ return true;
118
+ }
119
+ return WORKSPACE_CODEGEN_OWNERSHIP.cliStatic.directoryPrefixes.some(
120
+ (prefix) => path.startsWith(prefix),
121
+ );
122
+ }
123
+
124
+ export function isLibraryPath(filePath: string): boolean {
125
+ const path = normalizeProjectPath(filePath);
126
+ return isAuthoritativeGeneratedPath(path);
127
+ }
@@ -0,0 +1,169 @@
1
+ import type {
2
+ BoardCard,
3
+ GameTopologyManifest,
4
+ ManualCardSetDefinition,
5
+ ObjectSchema,
6
+ PresetCardSetDefinition,
7
+ PropertySchema,
8
+ } from "@dreamboard/sdk-types";
9
+
10
+ const STANDARD_DECK_ID = "standard_52_deck";
11
+
12
+ function createEnumPropertySchema(
13
+ enums: string[],
14
+ description: string,
15
+ ): PropertySchema {
16
+ return {
17
+ type: "enum",
18
+ description,
19
+ enums,
20
+ };
21
+ }
22
+
23
+ function createStringPropertySchema(description: string): PropertySchema {
24
+ return {
25
+ type: "string",
26
+ description,
27
+ };
28
+ }
29
+
30
+ export function createStandard52CardDeck(): ManualCardSetDefinition {
31
+ const suits = ["SPADES", "HEARTS", "CLUBS", "DIAMONDS"];
32
+ const ranks = [
33
+ "3",
34
+ "4",
35
+ "5",
36
+ "6",
37
+ "7",
38
+ "8",
39
+ "9",
40
+ "10",
41
+ "J",
42
+ "Q",
43
+ "K",
44
+ "A",
45
+ "2",
46
+ ];
47
+
48
+ const cards: BoardCard[] = [];
49
+ for (const suit of suits) {
50
+ for (const rank of ranks) {
51
+ cards.push({
52
+ type: `${suit}_${rank}`,
53
+ name: `${rank} of ${suit}`,
54
+ imageUrl: `/cards/${suit.toLowerCase()}_${rank.toLowerCase()}.png`,
55
+ text: `A playing card: ${rank} of ${suit}. Value: ${getBigTwoCardValue(rank)}, Suit value: ${getSuitValue(suit)}`,
56
+ count: 1,
57
+ cardType: `${suit}_${rank}`,
58
+ properties: {
59
+ suit,
60
+ rank,
61
+ },
62
+ });
63
+ }
64
+ }
65
+
66
+ return {
67
+ id: STANDARD_DECK_ID,
68
+ name: "Standard 52-Card Deck",
69
+ type: "manual",
70
+ cards,
71
+ cardSchema: {
72
+ properties: {
73
+ suit: createEnumPropertySchema(
74
+ ["SPADES", "HEARTS", "CLUBS", "DIAMONDS"],
75
+ "The suit of the card",
76
+ ),
77
+ rank: createStringPropertySchema("The rank of the card"),
78
+ },
79
+ } satisfies ObjectSchema,
80
+ };
81
+ }
82
+
83
+ export function addStandardDecksIfNeeded(
84
+ manifest: GameTopologyManifest,
85
+ ): GameTopologyManifest {
86
+ return {
87
+ ...manifest,
88
+ cardSets: manifest.cardSets.map((cardSet) =>
89
+ cardSet.type === "preset" && cardSet.presetId === STANDARD_DECK_ID
90
+ ? {
91
+ ...createStandard52CardDeck(),
92
+ id: cardSet.id,
93
+ name: cardSet.name,
94
+ }
95
+ : cardSet,
96
+ ),
97
+ };
98
+ }
99
+
100
+ export function materializePresetCardSet(
101
+ presetCardSet: PresetCardSetDefinition,
102
+ ): ManualCardSetDefinition {
103
+ if (presetCardSet.presetId !== STANDARD_DECK_ID) {
104
+ throw new Error(`Unsupported preset deck: ${presetCardSet.presetId}`);
105
+ }
106
+
107
+ const standardDeck = createStandard52CardDeck();
108
+ return {
109
+ ...standardDeck,
110
+ id: presetCardSet.id,
111
+ name: presetCardSet.name,
112
+ };
113
+ }
114
+
115
+ export function materializeCardSet(
116
+ cardSet: GameTopologyManifest["cardSets"][number],
117
+ ): ManualCardSetDefinition {
118
+ return cardSet.type === "manual"
119
+ ? cardSet
120
+ : materializePresetCardSet(cardSet);
121
+ }
122
+
123
+ function getBigTwoCardValue(rank: string): number {
124
+ switch (rank) {
125
+ case "3":
126
+ return 3;
127
+ case "4":
128
+ return 4;
129
+ case "5":
130
+ return 5;
131
+ case "6":
132
+ return 6;
133
+ case "7":
134
+ return 7;
135
+ case "8":
136
+ return 8;
137
+ case "9":
138
+ return 9;
139
+ case "10":
140
+ return 10;
141
+ case "J":
142
+ return 11;
143
+ case "Q":
144
+ return 12;
145
+ case "K":
146
+ return 13;
147
+ case "A":
148
+ return 14;
149
+ case "2":
150
+ return 15;
151
+ default:
152
+ return 0;
153
+ }
154
+ }
155
+
156
+ function getSuitValue(suit: string): number {
157
+ switch (suit) {
158
+ case "DIAMONDS":
159
+ return 1;
160
+ case "CLUBS":
161
+ return 2;
162
+ case "HEARTS":
163
+ return 3;
164
+ case "SPADES":
165
+ return 4;
166
+ default:
167
+ return 0;
168
+ }
169
+ }
@@ -0,0 +1,361 @@
1
+ import { readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { expect, test } from "bun:test";
4
+ import ts from "typescript";
5
+
6
+ const fixtureRoot = path.join(import.meta.dir, "__fixtures__", "sdk-types");
7
+ const workspaceCodegenRoot = path.resolve(import.meta.dir, "..");
8
+
9
+ const FIXTURE_FILES: readonly string[] = readdirSync(fixtureRoot)
10
+ .filter((name) => name.endsWith(".ts"))
11
+ .map((name) => path.join(fixtureRoot, name));
12
+
13
+ const COMPILER_OPTIONS: ts.CompilerOptions = {
14
+ noEmit: true,
15
+ strict: true,
16
+ target: ts.ScriptTarget.ES2022,
17
+ module: ts.ModuleKind.ESNext,
18
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
19
+ skipLibCheck: true,
20
+ };
21
+
22
+ const DIAGNOSTIC_FORMAT_HOST: ts.FormatDiagnosticsHost = {
23
+ getCanonicalFileName: (fileName) => fileName,
24
+ getCurrentDirectory: () => workspaceCodegenRoot,
25
+ getNewLine: () => "\n",
26
+ };
27
+
28
+ /**
29
+ * Shared TypeScript program covering every fixture file. Building one program
30
+ * per test process lets us reuse the parsed lib + SDK declaration graph across
31
+ * ~40 fixtures instead of spawning a cold `tsc --noEmit` per case
32
+ * (previously ~4s per spawn, ~120s total).
33
+ */
34
+ let cachedProgram: ts.Program | null = null;
35
+ function getProgram(): ts.Program {
36
+ if (!cachedProgram) {
37
+ cachedProgram = ts.createProgram({
38
+ rootNames: [...FIXTURE_FILES],
39
+ options: COMPILER_OPTIONS,
40
+ });
41
+ }
42
+ return cachedProgram;
43
+ }
44
+
45
+ function fixtureFilePath(fileName: string): string {
46
+ return path.join(fixtureRoot, fileName);
47
+ }
48
+
49
+ function getDiagnosticsForFixture(fileName: string): ts.Diagnostic[] {
50
+ const program = getProgram();
51
+ const targetPath = fixtureFilePath(fileName);
52
+ const sourceFile = program.getSourceFile(targetPath);
53
+ if (!sourceFile) {
54
+ throw new Error(
55
+ `Fixture not loaded into shared program: ${targetPath}. Ensure the file exists and is included in FIXTURE_FILES.`,
56
+ );
57
+ }
58
+ return [
59
+ ...program.getSyntacticDiagnostics(sourceFile),
60
+ ...program.getSemanticDiagnostics(sourceFile),
61
+ ];
62
+ }
63
+
64
+ function formatDiagnostics(diagnostics: readonly ts.Diagnostic[]): string {
65
+ return ts.formatDiagnostics(diagnostics, DIAGNOSTIC_FORMAT_HOST);
66
+ }
67
+
68
+ function expectFixtureTypechecks(fileName: string): void {
69
+ const diagnostics = getDiagnosticsForFixture(fileName);
70
+ if (diagnostics.length > 0) {
71
+ throw new Error(
72
+ `Typecheck failed for ${fileName}\n${formatDiagnostics(diagnostics)}`,
73
+ );
74
+ }
75
+ }
76
+
77
+ function expectTypecheckFailure(
78
+ fileName: string,
79
+ expectedSnippets: readonly string[],
80
+ ): void {
81
+ const diagnostics = getDiagnosticsForFixture(fileName);
82
+ const output = formatDiagnostics(diagnostics);
83
+
84
+ expect(diagnostics.length).toBeGreaterThan(0);
85
+ for (const snippet of expectedSnippets) {
86
+ expect(output).toContain(snippet);
87
+ }
88
+ }
89
+
90
+ test("defineTopologyManifest accepts valid typed references", () => {
91
+ expectFixtureTypechecks("valid-manifest.ts");
92
+ });
93
+
94
+ test("defineTopologyManifest accepts valid player-scoped seed homes with ownerId", () => {
95
+ expectFixtureTypechecks("valid-player-scoped-seed-homes.ts");
96
+ });
97
+
98
+ test("defineTopologyManifest accepts omitted boardTemplates", () => {
99
+ expectFixtureTypechecks("valid-manifest-omits-board-templates.ts");
100
+ });
101
+
102
+ test("defineTopologyManifest accepts die types that omit sides", () => {
103
+ expectFixtureTypechecks("valid-die-type-omits-sides.ts");
104
+ });
105
+
106
+ test("defineTopologyManifest rejects invalid typed references", () => {
107
+ expectTypecheckFailure("invalid-manifest.ts", [
108
+ "Type '\"discard\"' is not assignable to type '\"draw\"'",
109
+ "Type '\"missing-card-set\"' is not assignable to type '\"main\"'",
110
+ "Type '\"space-b\"' is not assignable to type '\"space-a\"'",
111
+ 'Type \'"missing-slot"\' is not assignable to type \'"worker-rest" | "staging"\'',
112
+ "Type '\"draft\"' is not assignable to type '\"standard\"'",
113
+ ]);
114
+ });
115
+
116
+ test("defineTopologyManifest rejects invalid strict slot host ids", () => {
117
+ expectTypecheckFailure("invalid-slot-host-manifest.ts", [
118
+ "Type '\"missing-host\"' is not assignable to type '\"mat-alpha\"'",
119
+ ]);
120
+ });
121
+
122
+ test("defineTopologyManifest rejects invalid strict slot ids", () => {
123
+ expectTypecheckFailure("invalid-slot-id-manifest.ts", [
124
+ "Type '\"missing-slot\"' is not assignable to type '\"staging\"'",
125
+ ]);
126
+ });
127
+
128
+ test("defineTopologyManifest rejects missing required card properties", () => {
129
+ expectTypecheckFailure("invalid-card-properties-missing-required.ts", [
130
+ "Property 'value' is missing",
131
+ ]);
132
+ });
133
+
134
+ test("defineTopologyManifest rejects extra card property keys", () => {
135
+ expectTypecheckFailure("invalid-card-properties-extra-key.ts", [
136
+ "'unexpected' does not exist in type",
137
+ ]);
138
+ });
139
+
140
+ test("defineTopologyManifest rejects wrong card scalar property values", () => {
141
+ expectTypecheckFailure("invalid-card-properties-wrong-scalar.ts", [
142
+ "Type 'string' is not assignable to type 'number'",
143
+ ]);
144
+ });
145
+
146
+ test("defineTopologyManifest rejects wrong card enum values", () => {
147
+ expectTypecheckFailure("invalid-card-properties-wrong-enum.ts", [
148
+ 'Type \'"stars"\' is not assignable to type \'"sun" | "moon"\'',
149
+ ]);
150
+ });
151
+
152
+ test("defineTopologyManifest rejects wrong nested card property values", () => {
153
+ expectTypecheckFailure("invalid-card-properties-wrong-nested.ts", [
154
+ "Type 'number' is not assignable to type 'string'",
155
+ ]);
156
+ });
157
+
158
+ test("defineTopologyManifest rejects wrong piece scalar field values", () => {
159
+ expectTypecheckFailure("invalid-piece-fields-wrong-scalar.ts", [
160
+ "Type 'string' is not assignable to type 'number'",
161
+ ]);
162
+ });
163
+
164
+ test("defineTopologyManifest rejects wrong piece enum field values", () => {
165
+ expectTypecheckFailure("invalid-piece-fields-wrong-enum.ts", [
166
+ 'Type \'"resting"\' is not assignable to type \'"ready" | "spent"\'',
167
+ ]);
168
+ });
169
+
170
+ test("defineTopologyManifest rejects wrong piece card id references", () => {
171
+ expectTypecheckFailure("invalid-piece-fields-wrong-card-id.ts", [
172
+ "Type '\"missing-card\"' is not assignable to type '\"ace\"'",
173
+ ]);
174
+ });
175
+
176
+ test("defineTopologyManifest rejects wrong piece zone id references", () => {
177
+ expectTypecheckFailure("invalid-piece-fields-wrong-zone-id.ts", [
178
+ "Type '\"discard\"' is not assignable to type '\"draw\"'",
179
+ ]);
180
+ });
181
+
182
+ test("defineTopologyManifest rejects extra piece field keys", () => {
183
+ expectTypecheckFailure("invalid-piece-fields-extra-key.ts", [
184
+ "'extra' does not exist in type",
185
+ ]);
186
+ });
187
+
188
+ test("defineTopologyManifest rejects per-player space homes without ownerId", () => {
189
+ expectTypecheckFailure("invalid-piece-home-per-player-space-no-owner.ts", [
190
+ "Property 'ownerId' is missing",
191
+ '"player-grid"',
192
+ ]);
193
+ });
194
+
195
+ test("defineTopologyManifest rejects per-player container homes without ownerId", () => {
196
+ expectTypecheckFailure(
197
+ "invalid-piece-home-per-player-container-no-owner.ts",
198
+ ["Property 'ownerId' is missing", '"player-grid"'],
199
+ );
200
+ });
201
+
202
+ test("defineTopologyManifest rejects per-player edge homes without ownerId", () => {
203
+ expectTypecheckFailure("invalid-piece-home-per-player-edge-no-owner.ts", [
204
+ "Property 'ownerId' is missing",
205
+ '"player-grid"',
206
+ ]);
207
+ });
208
+
209
+ test("defineTopologyManifest rejects per-player vertex homes without ownerId", () => {
210
+ expectTypecheckFailure("invalid-piece-home-per-player-vertex-no-owner.ts", [
211
+ "Property 'ownerId' is missing",
212
+ '"player-grid"',
213
+ ]);
214
+ });
215
+
216
+ test("defineTopologyManifest rejects piece seeds with unknown type ids", () => {
217
+ expectTypecheckFailure("invalid-piece-seed-type-id.ts", [
218
+ "Type '\"missing-meeple\"' is not assignable to type '\"meeple\"'",
219
+ ]);
220
+ });
221
+
222
+ test("defineTopologyManifest rejects wrong die player id references", () => {
223
+ expectTypecheckFailure("invalid-die-fields-wrong-player-id.ts", [
224
+ `Type '"player-3"' is not assignable to type`,
225
+ ]);
226
+ });
227
+
228
+ test("defineTopologyManifest rejects wrong die resource id references", () => {
229
+ expectTypecheckFailure("invalid-die-fields-wrong-resource-id.ts", [
230
+ "Type '\"missing-resource\"' is not assignable to type '\"supply\"'",
231
+ ]);
232
+ });
233
+
234
+ test("defineTopologyManifest rejects wrong die array item values", () => {
235
+ expectTypecheckFailure("invalid-die-fields-wrong-array-item.ts", [
236
+ "Type 'string' is not assignable to type 'number'",
237
+ ]);
238
+ });
239
+
240
+ test("defineTopologyManifest rejects die seeds with unknown type ids", () => {
241
+ expectTypecheckFailure("invalid-die-seed-type-id.ts", [
242
+ "Type '\"missing-d6\"' is not assignable to type '\"d6\"'",
243
+ ]);
244
+ });
245
+
246
+ test("defineTopologyManifest rejects per-player zone homes without ownerId", () => {
247
+ expectTypecheckFailure("invalid-die-home-per-player-zone-no-owner.ts", [
248
+ "Property 'ownerId' is missing",
249
+ '"main-hand"',
250
+ ]);
251
+ });
252
+
253
+ test("defineTopologyManifest rejects invalid board container card-set references", () => {
254
+ expectTypecheckFailure("invalid-container-card-set-manifest.ts", [
255
+ "Type '\"missing-card-set\"' is not assignable to type '\"main\"'",
256
+ ]);
257
+ });
258
+
259
+ test("defineTopologyManifest rejects edge ids in generic board field schemas", () => {
260
+ expectTypecheckFailure("invalid-generic-board-edge-id.ts", [
261
+ "not assignable to type 'never'",
262
+ ]);
263
+ });
264
+
265
+ test("defineTopologyManifest rejects invalid nested generic board fields", () => {
266
+ expectTypecheckFailure("invalid-generic-board-nested-field.ts", [
267
+ "Type 'number' is not assignable to type 'string'",
268
+ ]);
269
+ });
270
+
271
+ test("defineTopologyManifest rejects wrong square-board space ids in board fields", () => {
272
+ expectTypecheckFailure("invalid-square-board-space-id.ts", [
273
+ "missing-space",
274
+ "not assignable to type",
275
+ ]);
276
+ });
277
+
278
+ test("defineTopologyManifest rejects wrong square-board edge ids in board fields", () => {
279
+ expectTypecheckFailure("invalid-square-board-edge-id.ts", [
280
+ "square-edge:missing",
281
+ "not assignable to type",
282
+ ]);
283
+ });
284
+
285
+ test("defineTopologyManifest rejects wrong square-board vertex ids in board fields", () => {
286
+ expectTypecheckFailure("invalid-square-board-vertex-id.ts", [
287
+ "square-vertex:9,9",
288
+ "not assignable to type",
289
+ ]);
290
+ });
291
+
292
+ test("defineTopologyManifest rejects extra square-template space field keys", () => {
293
+ expectTypecheckFailure("invalid-square-space-fields-extra-key.ts", [
294
+ "'extra' does not exist in type",
295
+ ]);
296
+ });
297
+
298
+ test("defineTopologyManifest rejects wrong square-template space field enums", () => {
299
+ expectTypecheckFailure("invalid-square-space-fields-enum.ts", [
300
+ 'Type \'"lava"\' is not assignable to type \'"grass" | "water"\'',
301
+ ]);
302
+ });
303
+
304
+ test("defineTopologyManifest rejects wrong square relation space ids", () => {
305
+ expectTypecheckFailure("invalid-square-relation-space-id.ts", [
306
+ "missing-space",
307
+ "not assignable to type",
308
+ ]);
309
+ });
310
+
311
+ test("defineTopologyManifest rejects wrong square relation field scalars", () => {
312
+ expectTypecheckFailure("invalid-square-relation-field-scalar.ts", [
313
+ "Type 'string' is not assignable to type 'number'",
314
+ ]);
315
+ });
316
+
317
+ test("defineTopologyManifest rejects wrong square container host space ids", () => {
318
+ expectTypecheckFailure("invalid-square-container-host-space-id.ts", [
319
+ "missing-space",
320
+ "not assignable to type",
321
+ ]);
322
+ });
323
+
324
+ test("defineTopologyManifest rejects wrong square container field space ids", () => {
325
+ expectTypecheckFailure("invalid-square-container-field-space-id.ts", [
326
+ "missing-space",
327
+ "not assignable to type",
328
+ ]);
329
+ });
330
+
331
+ test("defineTopologyManifest rejects wrong hex edge ids in edge fields", () => {
332
+ expectTypecheckFailure("invalid-hex-edge-field-edge-id.ts", [
333
+ "hex-edge:missing",
334
+ "not assignable to type",
335
+ ]);
336
+ });
337
+
338
+ test("defineTopologyManifest rejects wrong hex vertex ids in vertex fields", () => {
339
+ expectTypecheckFailure("invalid-hex-vertex-field-vertex-id.ts", [
340
+ "hex-vertex:missing",
341
+ "not assignable to type",
342
+ ]);
343
+ });
344
+
345
+ test("defineTopologyManifest rejects invalid card visibility player ids", () => {
346
+ expectTypecheckFailure("invalid-card-visibility.ts", [
347
+ `Type '"player-3"' is not assignable to type`,
348
+ ]);
349
+ });
350
+
351
+ test("defineTopologyManifest rejects invalid piece visibility player ids", () => {
352
+ expectTypecheckFailure("invalid-piece-visibility.ts", [
353
+ `Type '"player-3"' is not assignable to type`,
354
+ ]);
355
+ });
356
+
357
+ test("defineTopologyManifest rejects invalid die visibility player ids", () => {
358
+ expectTypecheckFailure("invalid-die-visibility.ts", [
359
+ `Type '"player-3"' is not assignable to type`,
360
+ ]);
361
+ });