@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,490 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { CardInstance } from "../cards/card-instance";
3
+ import type { Modifier } from "../cards/modifiers";
4
+ import { createCardRegistry } from "../operations/card-registry-impl";
5
+ import { createCardId, createPlayerId, createZoneId } from "../types";
6
+ import type { ActionDefinition, ActionInstance } from "./action-definition";
7
+ import {
8
+ getAvailableActions,
9
+ hasAvailableActions,
10
+ type TimingContext,
11
+ validateAction,
12
+ validateActionTiming,
13
+ } from "./action-timing";
14
+
15
+ type TestGameState = TimingContext & {
16
+ cards: Record<string, CardInstance<{ modifiers: Modifier[] }>>;
17
+ turnCount?: number;
18
+ };
19
+
20
+ describe("Action Timing Validation", () => {
21
+ const player1 = createPlayerId("p1");
22
+ const player2 = createPlayerId("p2");
23
+ const card1 = createCardId("card1");
24
+ const card2 = createCardId("card2");
25
+ const playZone = createZoneId("play");
26
+
27
+ const registry = createCardRegistry([
28
+ { id: "creature1", name: "Test Creature", type: "creature" },
29
+ ]);
30
+
31
+ const baseState: TestGameState = {
32
+ currentSegment: "gameplay",
33
+ currentPhase: "mainPhase",
34
+ currentStep: null,
35
+ cards: {
36
+ [card1]: {
37
+ id: card1,
38
+ definitionId: "creature1",
39
+ owner: player1,
40
+ controller: player1,
41
+ zone: playZone,
42
+ tapped: false,
43
+ flipped: false,
44
+ revealed: false,
45
+ phased: false,
46
+ modifiers: [],
47
+ },
48
+ [card2]: {
49
+ id: card2,
50
+ definitionId: "creature1",
51
+ owner: player2,
52
+ controller: player2,
53
+ zone: playZone,
54
+ tapped: false,
55
+ flipped: false,
56
+ revealed: false,
57
+ phased: false,
58
+ modifiers: [],
59
+ },
60
+ },
61
+ };
62
+
63
+ describe("validateActionTiming", () => {
64
+ it("should allow action with no timing restrictions", () => {
65
+ const action: ActionDefinition = {
66
+ id: "pass",
67
+ name: "Pass",
68
+ };
69
+
70
+ const timingContext: TimingContext = {
71
+ currentSegment: "gameplay",
72
+ currentPhase: "mainPhase",
73
+ };
74
+
75
+ expect(validateActionTiming(action, timingContext)).toBe(true);
76
+ });
77
+
78
+ it("should validate segment restrictions", () => {
79
+ const action: ActionDefinition = {
80
+ id: "play-card",
81
+ name: "Play Card",
82
+ timing: {
83
+ segments: ["gameplay"],
84
+ },
85
+ };
86
+
87
+ expect(
88
+ validateActionTiming(action, {
89
+ currentSegment: "gameplay",
90
+ currentPhase: "mainPhase",
91
+ }),
92
+ ).toBe(true);
93
+
94
+ expect(
95
+ validateActionTiming(action, {
96
+ currentSegment: "setup",
97
+ currentPhase: "draft",
98
+ }),
99
+ ).toBe(false);
100
+ });
101
+
102
+ it("should validate phase restrictions", () => {
103
+ const action: ActionDefinition = {
104
+ id: "play-creature",
105
+ name: "Play Creature",
106
+ timing: {
107
+ phases: ["mainPhase"],
108
+ },
109
+ };
110
+
111
+ expect(
112
+ validateActionTiming(action, {
113
+ currentSegment: "gameplay",
114
+ currentPhase: "mainPhase",
115
+ }),
116
+ ).toBe(true);
117
+
118
+ expect(
119
+ validateActionTiming(action, {
120
+ currentSegment: "gameplay",
121
+ currentPhase: "combatPhase",
122
+ }),
123
+ ).toBe(false);
124
+ });
125
+
126
+ it("should validate step restrictions", () => {
127
+ const action: ActionDefinition = {
128
+ id: "attack",
129
+ name: "Attack",
130
+ timing: {
131
+ steps: ["attackStep"],
132
+ },
133
+ };
134
+
135
+ expect(
136
+ validateActionTiming(action, {
137
+ currentSegment: "gameplay",
138
+ currentPhase: "combatPhase",
139
+ currentStep: "attackStep",
140
+ }),
141
+ ).toBe(true);
142
+
143
+ expect(
144
+ validateActionTiming(action, {
145
+ currentSegment: "gameplay",
146
+ currentPhase: "combatPhase",
147
+ currentStep: "blockStep",
148
+ }),
149
+ ).toBe(false);
150
+ });
151
+
152
+ it("should validate multiple phases", () => {
153
+ const action: ActionDefinition = {
154
+ id: "instant-spell",
155
+ name: "Instant Spell",
156
+ timing: {
157
+ phases: ["mainPhase", "combatPhase", "endPhase"],
158
+ },
159
+ };
160
+
161
+ expect(
162
+ validateActionTiming(action, {
163
+ currentPhase: "mainPhase",
164
+ }),
165
+ ).toBe(true);
166
+ expect(
167
+ validateActionTiming(action, {
168
+ currentPhase: "combatPhase",
169
+ }),
170
+ ).toBe(true);
171
+ expect(
172
+ validateActionTiming(action, {
173
+ currentPhase: "drawPhase",
174
+ }),
175
+ ).toBe(false);
176
+ });
177
+
178
+ it("should validate custom timing predicates", () => {
179
+ type GameState = TimingContext & { turnCount: number };
180
+
181
+ const action: ActionDefinition<GameState> = {
182
+ id: "special-action",
183
+ name: "Special Action",
184
+ timing: {
185
+ custom: (state) => state.turnCount >= 5,
186
+ },
187
+ };
188
+
189
+ expect(
190
+ validateActionTiming(
191
+ action,
192
+ { currentSegment: "gameplay" },
193
+ { currentSegment: "gameplay", turnCount: 6 },
194
+ ),
195
+ ).toBe(true);
196
+
197
+ expect(
198
+ validateActionTiming(
199
+ action,
200
+ { currentSegment: "gameplay" },
201
+ { currentSegment: "gameplay", turnCount: 3 },
202
+ ),
203
+ ).toBe(false);
204
+ });
205
+
206
+ it("should combine segment, phase, and custom restrictions", () => {
207
+ type GameState = TimingContext & { hasSpecialToken: boolean };
208
+
209
+ const action: ActionDefinition<GameState> = {
210
+ id: "ultimate-ability",
211
+ name: "Ultimate Ability",
212
+ timing: {
213
+ segments: ["gameplay"],
214
+ phases: ["mainPhase"],
215
+ custom: (state) => state.hasSpecialToken,
216
+ },
217
+ };
218
+
219
+ expect(
220
+ validateActionTiming(
221
+ action,
222
+ { currentSegment: "gameplay", currentPhase: "mainPhase" },
223
+ {
224
+ currentSegment: "gameplay",
225
+ currentPhase: "mainPhase",
226
+ hasSpecialToken: true,
227
+ },
228
+ ),
229
+ ).toBe(true);
230
+
231
+ expect(
232
+ validateActionTiming(
233
+ action,
234
+ { currentSegment: "gameplay", currentPhase: "mainPhase" },
235
+ {
236
+ currentSegment: "gameplay",
237
+ currentPhase: "mainPhase",
238
+ hasSpecialToken: false,
239
+ },
240
+ ),
241
+ ).toBe(false);
242
+ });
243
+ });
244
+
245
+ describe("validateAction", () => {
246
+ it("should validate action without targets", () => {
247
+ const action: ActionDefinition = {
248
+ id: "draw",
249
+ name: "Draw Card",
250
+ timing: {
251
+ segments: ["gameplay"],
252
+ },
253
+ };
254
+
255
+ const instance: ActionInstance = {
256
+ actionId: "draw",
257
+ playerId: player1,
258
+ };
259
+
260
+ const result = validateAction(
261
+ instance,
262
+ action,
263
+ baseState,
264
+ baseState,
265
+ registry,
266
+ );
267
+
268
+ expect(result.valid).toBe(true);
269
+ });
270
+
271
+ it("should fail when timing is invalid", () => {
272
+ const action: ActionDefinition = {
273
+ id: "play-card",
274
+ name: "Play Card",
275
+ timing: {
276
+ segments: ["setup"], // Wrong segment
277
+ },
278
+ };
279
+
280
+ const instance: ActionInstance = {
281
+ actionId: "play-card",
282
+ playerId: player1,
283
+ };
284
+
285
+ const result = validateAction(
286
+ instance,
287
+ action,
288
+ baseState,
289
+ baseState,
290
+ registry,
291
+ );
292
+
293
+ expect(result.valid).toBe(false);
294
+ expect(result.reason).toBe("timing");
295
+ });
296
+
297
+ it("should fail when targets required but not provided", () => {
298
+ const action: ActionDefinition = {
299
+ id: "lightning-bolt",
300
+ name: "Lightning Bolt",
301
+ targets: [{ filter: { type: "creature" }, count: 1 }],
302
+ };
303
+
304
+ const instance: ActionInstance = {
305
+ actionId: "lightning-bolt",
306
+ playerId: player1,
307
+ };
308
+
309
+ const result = validateAction(
310
+ instance,
311
+ action,
312
+ baseState,
313
+ baseState,
314
+ registry,
315
+ );
316
+
317
+ expect(result.valid).toBe(false);
318
+ expect(result.reason).toBe("targets");
319
+ expect(result.error).toContain("requires targets");
320
+ });
321
+
322
+ it("should validate action with valid targets", () => {
323
+ const action: ActionDefinition = {
324
+ id: "lightning-bolt",
325
+ name: "Lightning Bolt",
326
+ targets: [{ filter: { type: "creature" }, count: 1 }],
327
+ };
328
+
329
+ const instance: ActionInstance = {
330
+ actionId: "lightning-bolt",
331
+ playerId: player1,
332
+ targets: [[card2]],
333
+ };
334
+
335
+ const result = validateAction(
336
+ instance,
337
+ action,
338
+ baseState,
339
+ baseState,
340
+ registry,
341
+ );
342
+
343
+ expect(result.valid).toBe(true);
344
+ });
345
+
346
+ it("should fail when target card does not exist", () => {
347
+ const action: ActionDefinition = {
348
+ id: "lightning-bolt",
349
+ name: "Lightning Bolt",
350
+ targets: [{ filter: { type: "creature" }, count: 1 }],
351
+ };
352
+
353
+ const nonexistentCard = createCardId("nonexistent");
354
+ const instance: ActionInstance = {
355
+ actionId: "lightning-bolt",
356
+ playerId: player1,
357
+ targets: [[nonexistentCard]],
358
+ };
359
+
360
+ const result = validateAction(
361
+ instance,
362
+ action,
363
+ baseState,
364
+ baseState,
365
+ registry,
366
+ );
367
+
368
+ expect(result.valid).toBe(false);
369
+ expect(result.reason).toBe("targets");
370
+ expect(result.error).toContain("do not exist");
371
+ });
372
+ });
373
+
374
+ describe("getAvailableActions", () => {
375
+ it("should return all actions when all are valid", () => {
376
+ const actions: ActionDefinition[] = [
377
+ { id: "pass", name: "Pass" },
378
+ {
379
+ id: "play",
380
+ name: "Play Card",
381
+ timing: { segments: ["gameplay"] },
382
+ },
383
+ ];
384
+
385
+ const timingContext: TimingContext = {
386
+ currentSegment: "gameplay",
387
+ };
388
+
389
+ const available = getAvailableActions(actions, timingContext);
390
+ expect(available).toHaveLength(2);
391
+ });
392
+
393
+ it("should filter out actions with invalid timing", () => {
394
+ const actions: ActionDefinition[] = [
395
+ {
396
+ id: "setup-action",
397
+ name: "Setup Action",
398
+ timing: { segments: ["setup"] },
399
+ },
400
+ {
401
+ id: "gameplay-action",
402
+ name: "Gameplay Action",
403
+ timing: { segments: ["gameplay"] },
404
+ },
405
+ ];
406
+
407
+ const timingContext: TimingContext = {
408
+ currentSegment: "gameplay",
409
+ };
410
+
411
+ const available = getAvailableActions(actions, timingContext);
412
+ expect(available).toHaveLength(1);
413
+ expect(available[0].id).toBe("gameplay-action");
414
+ });
415
+
416
+ it("should filter based on custom timing predicates", () => {
417
+ type GameState = TimingContext & { turnCount: number };
418
+
419
+ const actions: ActionDefinition<GameState>[] = [
420
+ {
421
+ id: "early-action",
422
+ name: "Early Action",
423
+ timing: { custom: (state) => state.turnCount < 5 },
424
+ },
425
+ {
426
+ id: "late-action",
427
+ name: "Late Action",
428
+ timing: { custom: (state) => state.turnCount >= 5 },
429
+ },
430
+ ];
431
+
432
+ const available = getAvailableActions(
433
+ actions,
434
+ { currentSegment: "gameplay" },
435
+ { currentSegment: "gameplay", turnCount: 6 },
436
+ );
437
+
438
+ expect(available).toHaveLength(1);
439
+ expect(available[0].id).toBe("late-action");
440
+ });
441
+ });
442
+
443
+ describe("hasAvailableActions", () => {
444
+ it("should return true when at least one action is available", () => {
445
+ const actions: ActionDefinition[] = [
446
+ {
447
+ id: "setup",
448
+ name: "Setup",
449
+ timing: { segments: ["setup"] },
450
+ },
451
+ {
452
+ id: "play",
453
+ name: "Play",
454
+ timing: { segments: ["gameplay"] },
455
+ },
456
+ ];
457
+
458
+ expect(hasAvailableActions(actions, { currentSegment: "gameplay" })).toBe(
459
+ true,
460
+ );
461
+ });
462
+
463
+ it("should return false when no actions are available", () => {
464
+ const actions: ActionDefinition[] = [
465
+ {
466
+ id: "setup1",
467
+ name: "Setup 1",
468
+ timing: { segments: ["setup"] },
469
+ },
470
+ {
471
+ id: "setup2",
472
+ name: "Setup 2",
473
+ timing: { segments: ["setup"] },
474
+ },
475
+ ];
476
+
477
+ expect(hasAvailableActions(actions, { currentSegment: "gameplay" })).toBe(
478
+ false,
479
+ );
480
+ });
481
+
482
+ it("should return true when actions have no timing restrictions", () => {
483
+ const actions: ActionDefinition[] = [{ id: "pass", name: "Pass" }];
484
+
485
+ expect(hasAvailableActions(actions, { currentSegment: "any" })).toBe(
486
+ true,
487
+ );
488
+ });
489
+ });
490
+ });