@drmxrcy/tcg-core 0.0.0-202602060542

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 (157) hide show
  1. package/README.md +882 -0
  2. package/package.json +58 -0
  3. package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
  4. package/src/__tests__/createMockAlphaClashGame.ts +462 -0
  5. package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
  6. package/src/__tests__/createMockGundamGame.ts +379 -0
  7. package/src/__tests__/createMockLorcanaGame.ts +328 -0
  8. package/src/__tests__/createMockOnePieceGame.ts +429 -0
  9. package/src/__tests__/createMockRiftboundGame.ts +462 -0
  10. package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
  11. package/src/__tests__/gundam-engine-definition.test.ts +110 -0
  12. package/src/__tests__/integration-complete-game.test.ts +508 -0
  13. package/src/__tests__/integration-network-sync.test.ts +469 -0
  14. package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
  15. package/src/__tests__/move-enumeration.test.ts +725 -0
  16. package/src/__tests__/multiplayer-engine.test.ts +555 -0
  17. package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
  18. package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
  19. package/src/actions/action-definition.test.ts +201 -0
  20. package/src/actions/action-definition.ts +122 -0
  21. package/src/actions/action-timing.test.ts +490 -0
  22. package/src/actions/action-timing.ts +257 -0
  23. package/src/cards/card-definition.test.ts +268 -0
  24. package/src/cards/card-definition.ts +27 -0
  25. package/src/cards/card-instance.test.ts +422 -0
  26. package/src/cards/card-instance.ts +49 -0
  27. package/src/cards/computed-properties.test.ts +530 -0
  28. package/src/cards/computed-properties.ts +84 -0
  29. package/src/cards/conditional-modifiers.test.ts +390 -0
  30. package/src/cards/modifiers.test.ts +286 -0
  31. package/src/cards/modifiers.ts +51 -0
  32. package/src/engine/MULTIPLAYER.md +425 -0
  33. package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
  34. package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
  35. package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
  36. package/src/engine/__tests__/rule-engine.test.ts +366 -0
  37. package/src/engine/index.ts +14 -0
  38. package/src/engine/multiplayer-engine.example.ts +571 -0
  39. package/src/engine/multiplayer-engine.ts +409 -0
  40. package/src/engine/rule-engine.test.ts +286 -0
  41. package/src/engine/rule-engine.ts +1539 -0
  42. package/src/engine/tracker-system.ts +172 -0
  43. package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
  44. package/src/filtering/card-filter.test.ts +230 -0
  45. package/src/filtering/card-filter.ts +91 -0
  46. package/src/filtering/card-query.test.ts +901 -0
  47. package/src/filtering/card-query.ts +273 -0
  48. package/src/filtering/filter-matching.test.ts +944 -0
  49. package/src/filtering/filter-matching.ts +315 -0
  50. package/src/flow/SERIALIZATION.md +428 -0
  51. package/src/flow/__tests__/flow-definition.test.ts +427 -0
  52. package/src/flow/__tests__/flow-manager.test.ts +756 -0
  53. package/src/flow/__tests__/flow-serialization.test.ts +565 -0
  54. package/src/flow/flow-definition.ts +453 -0
  55. package/src/flow/flow-manager.ts +1044 -0
  56. package/src/flow/index.ts +35 -0
  57. package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
  58. package/src/game-definition/__tests__/game-definition.test.ts +291 -0
  59. package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
  60. package/src/game-definition/game-definition.ts +261 -0
  61. package/src/game-definition/index.ts +28 -0
  62. package/src/game-definition/move-definitions.ts +188 -0
  63. package/src/game-definition/validation.ts +183 -0
  64. package/src/history/history-manager.test.ts +497 -0
  65. package/src/history/history-manager.ts +312 -0
  66. package/src/history/history-operations.ts +122 -0
  67. package/src/history/index.ts +9 -0
  68. package/src/history/types.ts +255 -0
  69. package/src/index.ts +32 -0
  70. package/src/logging/index.ts +27 -0
  71. package/src/logging/log-formatter.ts +187 -0
  72. package/src/logging/logger.ts +276 -0
  73. package/src/logging/types.ts +148 -0
  74. package/src/moves/create-move.test.ts +331 -0
  75. package/src/moves/create-move.ts +64 -0
  76. package/src/moves/move-enumeration.ts +228 -0
  77. package/src/moves/move-executor.test.ts +431 -0
  78. package/src/moves/move-executor.ts +195 -0
  79. package/src/moves/move-system.test.ts +380 -0
  80. package/src/moves/move-system.ts +463 -0
  81. package/src/moves/standard-moves.ts +231 -0
  82. package/src/operations/card-operations.test.ts +236 -0
  83. package/src/operations/card-operations.ts +116 -0
  84. package/src/operations/card-registry-impl.test.ts +251 -0
  85. package/src/operations/card-registry-impl.ts +70 -0
  86. package/src/operations/card-registry.test.ts +234 -0
  87. package/src/operations/card-registry.ts +106 -0
  88. package/src/operations/counter-operations.ts +152 -0
  89. package/src/operations/game-operations.test.ts +280 -0
  90. package/src/operations/game-operations.ts +140 -0
  91. package/src/operations/index.ts +24 -0
  92. package/src/operations/operations-impl.test.ts +354 -0
  93. package/src/operations/operations-impl.ts +468 -0
  94. package/src/operations/zone-operations.test.ts +295 -0
  95. package/src/operations/zone-operations.ts +223 -0
  96. package/src/rng/seeded-rng.test.ts +339 -0
  97. package/src/rng/seeded-rng.ts +123 -0
  98. package/src/targeting/index.ts +48 -0
  99. package/src/targeting/target-definition.test.ts +273 -0
  100. package/src/targeting/target-definition.ts +37 -0
  101. package/src/targeting/target-dsl.ts +279 -0
  102. package/src/targeting/target-resolver.ts +486 -0
  103. package/src/targeting/target-validation.test.ts +994 -0
  104. package/src/targeting/target-validation.ts +286 -0
  105. package/src/telemetry/events.ts +202 -0
  106. package/src/telemetry/index.ts +21 -0
  107. package/src/telemetry/telemetry-manager.ts +127 -0
  108. package/src/telemetry/types.ts +68 -0
  109. package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
  110. package/src/testing/index.ts +88 -0
  111. package/src/testing/test-assertions.test.ts +341 -0
  112. package/src/testing/test-assertions.ts +256 -0
  113. package/src/testing/test-card-factory.test.ts +228 -0
  114. package/src/testing/test-card-factory.ts +111 -0
  115. package/src/testing/test-context-factory.ts +187 -0
  116. package/src/testing/test-end-assertions.test.ts +262 -0
  117. package/src/testing/test-end-assertions.ts +95 -0
  118. package/src/testing/test-engine-builder.test.ts +389 -0
  119. package/src/testing/test-engine-builder.ts +46 -0
  120. package/src/testing/test-flow-assertions.test.ts +284 -0
  121. package/src/testing/test-flow-assertions.ts +115 -0
  122. package/src/testing/test-player-builder.test.ts +132 -0
  123. package/src/testing/test-player-builder.ts +46 -0
  124. package/src/testing/test-replay-assertions.test.ts +356 -0
  125. package/src/testing/test-replay-assertions.ts +164 -0
  126. package/src/testing/test-rng-helpers.test.ts +260 -0
  127. package/src/testing/test-rng-helpers.ts +190 -0
  128. package/src/testing/test-state-builder.test.ts +373 -0
  129. package/src/testing/test-state-builder.ts +99 -0
  130. package/src/testing/test-zone-factory.test.ts +295 -0
  131. package/src/testing/test-zone-factory.ts +224 -0
  132. package/src/types/branded-utils.ts +54 -0
  133. package/src/types/branded.test.ts +175 -0
  134. package/src/types/branded.ts +33 -0
  135. package/src/types/index.ts +8 -0
  136. package/src/types/state.test.ts +198 -0
  137. package/src/types/state.ts +154 -0
  138. package/src/validation/card-type-guards.test.ts +242 -0
  139. package/src/validation/card-type-guards.ts +179 -0
  140. package/src/validation/index.ts +40 -0
  141. package/src/validation/schema-builders.test.ts +403 -0
  142. package/src/validation/schema-builders.ts +345 -0
  143. package/src/validation/type-guard-builder.test.ts +216 -0
  144. package/src/validation/type-guard-builder.ts +109 -0
  145. package/src/validation/validator-builder.test.ts +375 -0
  146. package/src/validation/validator-builder.ts +273 -0
  147. package/src/zones/index.ts +28 -0
  148. package/src/zones/zone-factory.test.ts +183 -0
  149. package/src/zones/zone-factory.ts +44 -0
  150. package/src/zones/zone-operations.test.ts +800 -0
  151. package/src/zones/zone-operations.ts +306 -0
  152. package/src/zones/zone-state-helpers.test.ts +337 -0
  153. package/src/zones/zone-state-helpers.ts +128 -0
  154. package/src/zones/zone-visibility.test.ts +156 -0
  155. package/src/zones/zone-visibility.ts +36 -0
  156. package/src/zones/zone.test.ts +186 -0
  157. package/src/zones/zone.ts +66 -0
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Zod schema composition utilities for building complex validation schemas
3
+ *
4
+ * Provides helper functions for composing, extending, and merging Zod schemas
5
+ * in a type-safe and reusable way.
6
+ */
7
+
8
+ import { type ZodObject, type ZodRawShape, type ZodTypeAny, z } from "zod";
9
+
10
+ /**
11
+ * Creates a card schema from a shape object
12
+ *
13
+ * This is a thin wrapper around z.object() that provides better semantics
14
+ * for card-related schemas and can be extended with additional features.
15
+ *
16
+ * @param shape - The shape of the schema
17
+ * @returns A Zod object schema
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const cardSchema = createCardSchema({
22
+ * id: z.string(),
23
+ * name: z.string(),
24
+ * type: z.string(),
25
+ * basePower: z.number().optional(),
26
+ * });
27
+ *
28
+ * type Card = z.infer<typeof cardSchema>;
29
+ * ```
30
+ */
31
+ export function createCardSchema<T extends ZodRawShape>(
32
+ shape: T,
33
+ ): ZodObject<T> {
34
+ return z.object(shape);
35
+ }
36
+
37
+ /**
38
+ * Extends a base schema with additional fields
39
+ *
40
+ * Creates a new schema that includes all fields from the base schema
41
+ * plus the new fields from the extension.
42
+ *
43
+ * @param baseSchema - The base schema to extend
44
+ * @param extension - Additional fields to add
45
+ * @returns A new schema with combined fields
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const baseCardSchema = createCardSchema({
50
+ * id: z.string(),
51
+ * name: z.string(),
52
+ * });
53
+ *
54
+ * const creatureSchema = extendSchema(baseCardSchema, {
55
+ * power: z.number(),
56
+ * toughness: z.number(),
57
+ * });
58
+ * ```
59
+ */
60
+ export function extendSchema<T extends ZodRawShape, E extends ZodRawShape>(
61
+ baseSchema: ZodObject<T>,
62
+ extension: E,
63
+ ): ZodObject<any> {
64
+ return baseSchema.extend(extension) as ZodObject<any>;
65
+ }
66
+
67
+ /**
68
+ * Merges multiple schemas into one
69
+ *
70
+ * Combines all fields from all provided schemas into a single schema.
71
+ * Later schemas can override fields from earlier schemas.
72
+ *
73
+ * @param schemas - Schemas to merge (at least 2)
74
+ * @returns A new schema with all fields merged
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const baseSchema = createCardSchema({ id: z.string() });
79
+ * const typeSchema = createCardSchema({ type: z.string() });
80
+ * const statsSchema = createCardSchema({ power: z.number() });
81
+ *
82
+ * const fullSchema = mergeSchemas(baseSchema, typeSchema, statsSchema);
83
+ * ```
84
+ */
85
+ export function mergeSchemas<T extends ZodRawShape, U extends ZodRawShape>(
86
+ schema1: ZodObject<T>,
87
+ schema2: ZodObject<U>,
88
+ ): ZodObject<any>;
89
+
90
+ export function mergeSchemas<
91
+ T extends ZodRawShape,
92
+ U extends ZodRawShape,
93
+ V extends ZodRawShape,
94
+ >(
95
+ schema1: ZodObject<T>,
96
+ schema2: ZodObject<U>,
97
+ schema3: ZodObject<V>,
98
+ ): ZodObject<any>;
99
+
100
+ export function mergeSchemas<
101
+ T extends ZodRawShape,
102
+ U extends ZodRawShape,
103
+ V extends ZodRawShape,
104
+ W extends ZodRawShape,
105
+ >(
106
+ schema1: ZodObject<T>,
107
+ schema2: ZodObject<U>,
108
+ schema3: ZodObject<V>,
109
+ schema4: ZodObject<W>,
110
+ ): ZodObject<any>;
111
+
112
+ export function mergeSchemas(
113
+ ...schemas: ZodObject<ZodRawShape>[]
114
+ ): ZodObject<any> {
115
+ if (schemas.length < 2) {
116
+ throw new Error("mergeSchemas requires at least 2 schemas");
117
+ }
118
+
119
+ return schemas.reduce((acc, schema) => acc.merge(schema)) as ZodObject<any>;
120
+ }
121
+
122
+ /**
123
+ * Composes multiple schemas by intersecting them
124
+ *
125
+ * Creates a schema that validates against all provided schemas.
126
+ * This is useful when you want to ensure data matches multiple independent schemas.
127
+ *
128
+ * @param schemas - Array of schemas to compose
129
+ * @returns A composed schema
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const hasId = z.object({ id: z.string() });
134
+ * const hasName = z.object({ name: z.string() });
135
+ * const hasType = z.object({ type: z.string() });
136
+ *
137
+ * const fullSchema = composeSchemas([hasId, hasName, hasType]);
138
+ * ```
139
+ */
140
+ export function composeSchemas(schemas: ZodTypeAny[]): ZodTypeAny {
141
+ if (schemas.length === 0) {
142
+ throw new Error("composeSchemas requires at least 1 schema");
143
+ }
144
+
145
+ if (schemas.length === 1) {
146
+ return schemas[0];
147
+ }
148
+
149
+ // Use merge for object schemas
150
+ return schemas.reduce((acc: ZodTypeAny, schema: ZodTypeAny): ZodTypeAny => {
151
+ if (acc instanceof z.ZodObject && schema instanceof z.ZodObject) {
152
+ return acc.merge(schema) as ZodTypeAny;
153
+ }
154
+ return z.intersection(acc, schema);
155
+ }) as ZodTypeAny;
156
+ }
157
+
158
+ /**
159
+ * Makes all fields in a schema optional
160
+ *
161
+ * Creates a new schema where all fields from the original schema
162
+ * are optional. Useful for partial updates or patch operations.
163
+ *
164
+ * @param schema - The schema to make optional
165
+ * @returns A schema with all fields optional
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * const requiredSchema = createCardSchema({
170
+ * id: z.string(),
171
+ * name: z.string(),
172
+ * type: z.string(),
173
+ * });
174
+ *
175
+ * const optionalSchema = createOptionalSchema(requiredSchema);
176
+ * // Now id, name, and type are all optional
177
+ * ```
178
+ */
179
+ export function createOptionalSchema<T extends ZodRawShape>(
180
+ schema: ZodObject<T>,
181
+ ): ZodObject<{ [K in keyof T]: ZodTypeAny }> {
182
+ return schema.partial() as ZodObject<{ [K in keyof T]: ZodTypeAny }>;
183
+ }
184
+
185
+ /**
186
+ * Creates a strict schema that doesn't allow extra fields
187
+ *
188
+ * By default, Zod schemas allow extra fields. This function creates
189
+ * a schema that will reject objects with fields not defined in the schema.
190
+ *
191
+ * @param shape - The shape of the schema
192
+ * @returns A strict schema
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * const schema = createStrictSchema({
197
+ * id: z.string(),
198
+ * name: z.string(),
199
+ * });
200
+ *
201
+ * // This will fail because of the extra field
202
+ * schema.parse({ id: "1", name: "Card", extra: "not allowed" });
203
+ * ```
204
+ */
205
+ export function createStrictSchema<T extends ZodRawShape>(
206
+ shape: T,
207
+ ): ZodObject<T> {
208
+ return z.object(shape).strict();
209
+ }
210
+
211
+ /**
212
+ * Creates a schema for validating arrays of items
213
+ *
214
+ * @param itemSchema - Schema for individual items
215
+ * @param options - Optional constraints (min, max length)
216
+ * @returns A schema for validating arrays
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * const cardSchema = createCardSchema({
221
+ * id: z.string(),
222
+ * name: z.string(),
223
+ * });
224
+ *
225
+ * const deckSchema = createArraySchema(cardSchema, { min: 40, max: 60 });
226
+ * ```
227
+ */
228
+ export function createArraySchema<T extends ZodTypeAny>(
229
+ itemSchema: T,
230
+ options?: { min?: number; max?: number },
231
+ ): z.ZodArray<T> {
232
+ let arraySchema = z.array(itemSchema);
233
+
234
+ if (options?.min !== undefined) {
235
+ arraySchema = arraySchema.min(options.min);
236
+ }
237
+
238
+ if (options?.max !== undefined) {
239
+ arraySchema = arraySchema.max(options.max);
240
+ }
241
+
242
+ return arraySchema;
243
+ }
244
+
245
+ /**
246
+ * Creates a discriminated union schema
247
+ *
248
+ * Useful for validating objects that have different shapes based on a discriminator field.
249
+ *
250
+ * @param discriminator - The field that determines which schema to use
251
+ * @param schemas - Map of discriminator values to schemas
252
+ * @returns A discriminated union schema
253
+ *
254
+ * @example
255
+ * ```typescript
256
+ * const cardSchema = createDiscriminatedUnion("type", [
257
+ * z.object({ type: z.literal("creature"), power: z.number() }),
258
+ * z.object({ type: z.literal("instant"), damage: z.number() }),
259
+ * ]);
260
+ * ```
261
+ */
262
+ export function createDiscriminatedUnion<K extends string>(
263
+ discriminator: K,
264
+ schemas: [ZodObject<any>, ZodObject<any>, ...ZodObject<any>[]],
265
+ ): z.ZodDiscriminatedUnion<K, any> {
266
+ return z.discriminatedUnion(discriminator, schemas as any);
267
+ }
268
+
269
+ /**
270
+ * Creates a record schema for dynamic key-value mappings
271
+ *
272
+ * @param keySchema - Schema for the keys
273
+ * @param valueSchema - Schema for the values
274
+ * @returns A record schema
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * // Map of player IDs to scores
279
+ * const scoresSchema = createRecordSchema(z.string(), z.number());
280
+ * ```
281
+ */
282
+ export function createRecordSchema<
283
+ K extends z.ZodTypeAny,
284
+ V extends z.ZodTypeAny,
285
+ >(keySchema: K, valueSchema: V): z.ZodRecord<K, V> {
286
+ return z.record(keySchema, valueSchema);
287
+ }
288
+
289
+ /**
290
+ * Creates a schema with custom refinements and error messages
291
+ *
292
+ * @param baseSchema - The base schema to refine
293
+ * @param refinement - Refinement function
294
+ * @param message - Error message if refinement fails
295
+ * @returns A refined schema
296
+ *
297
+ * @example
298
+ * ```typescript
299
+ * const powerSchema = z.number();
300
+ * const positivePowerSchema = createRefinedSchema(
301
+ * powerSchema,
302
+ * (val) => val > 0,
303
+ * "Power must be positive"
304
+ * );
305
+ * ```
306
+ */
307
+ export function createRefinedSchema<T extends ZodTypeAny>(
308
+ baseSchema: T,
309
+ refinement: (val: z.infer<T>) => boolean,
310
+ message: string,
311
+ ): z.ZodEffects<T> {
312
+ return baseSchema.refine(refinement, { message });
313
+ }
314
+
315
+ /**
316
+ * Creates a schema with multiple refinements
317
+ *
318
+ * @param baseSchema - The base schema to refine
319
+ * @param refinements - Array of refinements with messages
320
+ * @returns A refined schema
321
+ *
322
+ * @example
323
+ * ```typescript
324
+ * const passwordSchema = createMultiRefinedSchema(z.string(), [
325
+ * { check: (val) => val.length >= 8, message: "Must be at least 8 characters" },
326
+ * { check: (val) => /[A-Z]/.test(val), message: "Must contain uppercase" },
327
+ * { check: (val) => /[0-9]/.test(val), message: "Must contain number" },
328
+ * ]);
329
+ * ```
330
+ */
331
+ export function createMultiRefinedSchema<T extends ZodTypeAny>(
332
+ baseSchema: T,
333
+ refinements: Array<{
334
+ check: (val: z.infer<T>) => boolean;
335
+ message: string;
336
+ }>,
337
+ ): ZodTypeAny {
338
+ let schema: ZodTypeAny = baseSchema;
339
+
340
+ for (const { check, message } of refinements) {
341
+ schema = schema.refine(check, { message });
342
+ }
343
+
344
+ return schema;
345
+ }
@@ -0,0 +1,216 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createTypeGuard } from "./type-guard-builder";
3
+
4
+ describe("createTypeGuard", () => {
5
+ describe("basic type guards", () => {
6
+ it("should create a type guard for a string field", () => {
7
+ type Card = { type: string; name: string };
8
+ const isCreature = createTypeGuard<Card, "type", "creature">(
9
+ "type",
10
+ "creature",
11
+ );
12
+
13
+ const creature: Card = { type: "creature", name: "Dragon" };
14
+ const instant: Card = { type: "instant", name: "Lightning Bolt" };
15
+
16
+ expect(isCreature(creature)).toBe(true);
17
+ expect(isCreature(instant)).toBe(false);
18
+ });
19
+
20
+ it("should create a type guard for a number field", () => {
21
+ type Item = { id: number; category: string };
22
+ const isItem42 = createTypeGuard<Item, "id", 42>("id", 42);
23
+
24
+ const item42: Item = { id: 42, category: "test" };
25
+ const item10: Item = { id: 10, category: "test" };
26
+
27
+ expect(isItem42(item42)).toBe(true);
28
+ expect(isItem42(item10)).toBe(false);
29
+ });
30
+
31
+ it("should create a type guard for a boolean field", () => {
32
+ type Config = { enabled: boolean; name: string };
33
+ const isEnabled = createTypeGuard<Config, "enabled", true>(
34
+ "enabled",
35
+ true,
36
+ );
37
+
38
+ const enabledConfig: Config = { enabled: true, name: "test" };
39
+ const disabledConfig: Config = { enabled: false, name: "test" };
40
+
41
+ expect(isEnabled(enabledConfig)).toBe(true);
42
+ expect(isEnabled(disabledConfig)).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe("complex types", () => {
47
+ it("should work with union types", () => {
48
+ type Card = { type: "creature" | "instant" | "sorcery"; name: string };
49
+ const isCreature = createTypeGuard<Card, "type", "creature">(
50
+ "type",
51
+ "creature",
52
+ );
53
+ const isInstant = createTypeGuard<Card, "type", "instant">(
54
+ "type",
55
+ "instant",
56
+ );
57
+
58
+ const creature: Card = { type: "creature", name: "Dragon" };
59
+ const instant: Card = { type: "instant", name: "Lightning Bolt" };
60
+
61
+ expect(isCreature(creature)).toBe(true);
62
+ expect(isCreature(instant)).toBe(false);
63
+ expect(isInstant(instant)).toBe(true);
64
+ expect(isInstant(creature)).toBe(false);
65
+ });
66
+
67
+ it("should work with optional fields", () => {
68
+ type Card = { type?: string; name: string };
69
+ const isCreature = createTypeGuard<Card, "type", "creature">(
70
+ "type",
71
+ "creature",
72
+ );
73
+
74
+ const creature: Card = { type: "creature", name: "Dragon" };
75
+ const noType: Card = { name: "Unknown" };
76
+
77
+ expect(isCreature(creature)).toBe(true);
78
+ expect(isCreature(noType)).toBe(false);
79
+ });
80
+
81
+ it("should work with nested objects", () => {
82
+ type Card = {
83
+ metadata: {
84
+ category: string;
85
+ version: number;
86
+ };
87
+ name: string;
88
+ };
89
+ const isStandard = createTypeGuard<
90
+ Card,
91
+ "metadata",
92
+ { category: "standard"; version: 1 }
93
+ >("metadata", { category: "standard", version: 1 });
94
+
95
+ const standardCard: Card = {
96
+ metadata: { category: "standard", version: 1 },
97
+ name: "Card",
98
+ };
99
+ const modernCard: Card = {
100
+ metadata: { category: "modern", version: 1 },
101
+ name: "Card",
102
+ };
103
+
104
+ expect(isStandard(standardCard)).toBe(true);
105
+ expect(isStandard(modernCard)).toBe(false);
106
+ });
107
+ });
108
+
109
+ describe("type narrowing", () => {
110
+ it("should narrow types correctly in TypeScript", () => {
111
+ type Card = {
112
+ type: "creature" | "instant";
113
+ name: string;
114
+ power?: number;
115
+ };
116
+ const isCreature = createTypeGuard<Card, "type", "creature">(
117
+ "type",
118
+ "creature",
119
+ );
120
+
121
+ const card: Card = { type: "creature", name: "Dragon", power: 5 };
122
+
123
+ if (isCreature(card)) {
124
+ // TypeScript should know that card.type is "creature" here
125
+ const typeValue: "creature" = card.type;
126
+ expect(typeValue).toBe("creature");
127
+ }
128
+ });
129
+ });
130
+
131
+ describe("edge cases", () => {
132
+ it("should handle null and undefined gracefully", () => {
133
+ type Card = { type: string | null | undefined; name: string };
134
+ const isCreature = createTypeGuard<Card, "type", "creature">(
135
+ "type",
136
+ "creature",
137
+ );
138
+
139
+ const nullCard: Card = { type: null, name: "Card" };
140
+ const undefinedCard: Card = { type: undefined, name: "Card" };
141
+ const validCard: Card = { type: "creature", name: "Card" };
142
+
143
+ expect(isCreature(nullCard)).toBe(false);
144
+ expect(isCreature(undefinedCard)).toBe(false);
145
+ expect(isCreature(validCard)).toBe(true);
146
+ });
147
+
148
+ it("should handle empty strings", () => {
149
+ type Card = { type: string; name: string };
150
+ const isEmpty = createTypeGuard<Card, "type", "">("type", "");
151
+
152
+ const emptyCard: Card = { type: "", name: "Card" };
153
+ const nonEmptyCard: Card = { type: "creature", name: "Card" };
154
+
155
+ expect(isEmpty(emptyCard)).toBe(true);
156
+ expect(isEmpty(nonEmptyCard)).toBe(false);
157
+ });
158
+
159
+ it("should handle objects without the specified field", () => {
160
+ type PartialCard = { name: string; type?: string };
161
+ const isCreature = createTypeGuard<PartialCard, "type", "creature">(
162
+ "type",
163
+ "creature",
164
+ );
165
+
166
+ const card: PartialCard = { name: "Card" };
167
+
168
+ expect(isCreature(card)).toBe(false);
169
+ });
170
+ });
171
+
172
+ describe("array values", () => {
173
+ it("should create a type guard for array field values", () => {
174
+ type Card = { types: string[]; name: string };
175
+ const hasCreatureTypes = createTypeGuard<Card, "types", string[]>(
176
+ "types",
177
+ ["creature", "dragon"],
178
+ );
179
+
180
+ const dragonCard: Card = {
181
+ types: ["creature", "dragon"],
182
+ name: "Dragon",
183
+ };
184
+ const goblinCard: Card = {
185
+ types: ["creature", "goblin"],
186
+ name: "Goblin",
187
+ };
188
+
189
+ expect(hasCreatureTypes(dragonCard)).toBe(true);
190
+ expect(hasCreatureTypes(goblinCard)).toBe(false);
191
+ });
192
+ });
193
+
194
+ describe("performance", () => {
195
+ it("should be efficient for multiple checks", () => {
196
+ type Card = { type: string; name: string };
197
+ const isCreature = createTypeGuard<Card, "type", "creature">(
198
+ "type",
199
+ "creature",
200
+ );
201
+
202
+ const creature: Card = { type: "creature", name: "Dragon" };
203
+ const instant: Card = { type: "instant", name: "Lightning Bolt" };
204
+
205
+ const startTime = performance.now();
206
+ for (let i = 0; i < 10000; i++) {
207
+ isCreature(creature);
208
+ isCreature(instant);
209
+ }
210
+ const endTime = performance.now();
211
+
212
+ // Should complete in reasonable time (< 1000ms for 20k checks, higher threshold for CI parallel execution)
213
+ expect(endTime - startTime).toBeLessThan(1000);
214
+ });
215
+ });
216
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Type guard builder utilities for creating type narrowing functions
3
+ *
4
+ * These utilities help create type-safe predicates that TypeScript can use
5
+ * for type narrowing in conditional blocks.
6
+ */
7
+
8
+ /**
9
+ * Creates a type guard function that checks if an object's field matches a specific value
10
+ *
11
+ * @template T - The object type to guard
12
+ * @template K - The key of the field to check (must be a key of T)
13
+ * @template V - The value type to check against
14
+ *
15
+ * @param field - The field name to check
16
+ * @param value - The value to compare against
17
+ * @returns A type guard function that narrows T based on the field value
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * type Card = { type: "creature" | "instant"; name: string };
22
+ * const isCreature = createTypeGuard<Card, "type", "creature">("type", "creature");
23
+ *
24
+ * const card: Card = { type: "creature", name: "Dragon" };
25
+ * if (isCreature(card)) {
26
+ * // TypeScript knows card.type is "creature" here
27
+ * console.log(card.type); // Type: "creature"
28
+ * }
29
+ * ```
30
+ */
31
+ export function createTypeGuard<T, K extends keyof T, V extends T[K]>(
32
+ field: K,
33
+ value: V,
34
+ ): (obj: T) => obj is T & Record<K, V> {
35
+ return (obj: T): obj is T & Record<K, V> => {
36
+ // Handle null and undefined gracefully
37
+ if (obj === null || obj === undefined) {
38
+ return false;
39
+ }
40
+
41
+ const fieldValue = obj[field];
42
+
43
+ // Handle primitive comparisons
44
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
45
+ // Deep equality check for objects
46
+ return deepEqual(fieldValue, value);
47
+ }
48
+
49
+ if (Array.isArray(value)) {
50
+ // Deep equality check for arrays
51
+ return deepEqual(fieldValue, value);
52
+ }
53
+
54
+ // Simple equality check for primitives
55
+ return fieldValue === value;
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Deep equality comparison for objects and arrays
61
+ * Used internally by createTypeGuard for complex value comparisons
62
+ *
63
+ * @param a - First value to compare
64
+ * @param b - Second value to compare
65
+ * @returns true if values are deeply equal, false otherwise
66
+ */
67
+ function deepEqual(a: unknown, b: unknown): boolean {
68
+ // Handle primitive types
69
+ if (a === b) {
70
+ return true;
71
+ }
72
+
73
+ // Handle null and undefined
74
+ if (a === null || b === null || a === undefined || b === undefined) {
75
+ return a === b;
76
+ }
77
+
78
+ // Handle different types
79
+ if (typeof a !== typeof b) {
80
+ return false;
81
+ }
82
+
83
+ // Handle arrays
84
+ if (Array.isArray(a) && Array.isArray(b)) {
85
+ if (a.length !== b.length) {
86
+ return false;
87
+ }
88
+ return a.every((item, index) => deepEqual(item, b[index]));
89
+ }
90
+
91
+ // Handle objects
92
+ if (typeof a === "object" && typeof b === "object") {
93
+ const keysA = Object.keys(a as object);
94
+ const keysB = Object.keys(b as object);
95
+
96
+ if (keysA.length !== keysB.length) {
97
+ return false;
98
+ }
99
+
100
+ return keysA.every((key) =>
101
+ deepEqual(
102
+ (a as Record<string, unknown>)[key],
103
+ (b as Record<string, unknown>)[key],
104
+ ),
105
+ );
106
+ }
107
+
108
+ return false;
109
+ }