@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,462 @@
1
+ import type { FlowDefinition } from "../flow";
2
+ import type { GameDefinition, GameMoveDefinitions } from "../game-definition";
3
+ import { standardMoves } from "../moves/standard-moves";
4
+ import type { CardId, PlayerId, ZoneId } from "../types";
5
+ import type { CardZoneConfig } from "../zones";
6
+
7
+ // Mock Riftbound game state - SIMPLIFIED!
8
+ type TestGameState = {
9
+ victoryPoints: Record<string, number>;
10
+ battlefieldControl: Record<string, PlayerId | null>;
11
+ runePools: Record<
12
+ string,
13
+ {
14
+ energy: number;
15
+ power: Record<string, number>;
16
+ }
17
+ >;
18
+ conqueredThisTurn: Record<string, CardId[]>;
19
+ };
20
+
21
+ type TestMoves = {
22
+ // Setup moves
23
+ initializeDecks: { playerId: PlayerId };
24
+ placeLegend: { playerId: PlayerId; legendId: CardId };
25
+ placeChampion: { playerId: PlayerId; championId: CardId };
26
+ placeBattlefields: { battlefieldIds: CardId[] };
27
+ shuffleDecks: { playerId: PlayerId };
28
+ drawInitialHand: { playerId: PlayerId };
29
+ transitionToPlay: Record<string, never>;
30
+ // Regular game moves
31
+ channelRunes: { playerId: PlayerId; count: number };
32
+ drawCard: { playerId: PlayerId };
33
+ playUnit: { playerId: PlayerId; cardId: CardId };
34
+ playGear: { playerId: PlayerId; cardId: CardId };
35
+ playSpell: { playerId: PlayerId; cardId: CardId; targets?: CardId[] };
36
+ moveUnit: { playerId: PlayerId; unitId: CardId; targetLocation: string };
37
+ initiateCombat: { playerId: PlayerId; battlefieldId: CardId };
38
+ // Standard moves
39
+ pass: { playerId: PlayerId };
40
+ concede: { playerId: PlayerId };
41
+ };
42
+
43
+ // Riftbound move definitions
44
+ const riftboundMoves: GameMoveDefinitions<TestGameState, TestMoves> = {
45
+ // Setup moves using engine utilities
46
+ initializeDecks: {
47
+ reducer: (_draft, context) => {
48
+ const { zones } = context;
49
+ const playerId = context.params.playerId;
50
+
51
+ // Use engine's createDeck utility!
52
+ zones.createDeck({
53
+ zoneId: "mainDeck" as ZoneId,
54
+ playerId,
55
+ cardCount: 40,
56
+ shuffle: false,
57
+ });
58
+
59
+ zones.createDeck({
60
+ zoneId: "runeDeck" as ZoneId,
61
+ playerId,
62
+ cardCount: 12,
63
+ shuffle: false,
64
+ });
65
+
66
+ // NO MORE: draft.setupStep
67
+ },
68
+ },
69
+
70
+ placeLegend: {
71
+ reducer: (_draft, context) => {
72
+ const { zones } = context;
73
+ const legendId = context.params.legendId;
74
+
75
+ zones.moveCard({
76
+ cardId: legendId,
77
+ targetZoneId: "legendZone" as ZoneId,
78
+ });
79
+
80
+ // NO MORE: draft.setupStep
81
+ },
82
+ },
83
+
84
+ placeChampion: {
85
+ reducer: (_draft, context) => {
86
+ const { zones } = context;
87
+ const championId = context.params.championId;
88
+
89
+ zones.moveCard({
90
+ cardId: championId,
91
+ targetZoneId: "championZone" as ZoneId,
92
+ });
93
+
94
+ // NO MORE: draft.setupStep
95
+ },
96
+ },
97
+
98
+ placeBattlefields: {
99
+ reducer: (draft, context) => {
100
+ const { zones } = context;
101
+ const battlefieldIds = context.params.battlefieldIds;
102
+
103
+ // Place each battlefield
104
+ for (const battlefieldId of battlefieldIds) {
105
+ zones.moveCard({
106
+ cardId: battlefieldId as CardId,
107
+ targetZoneId: "battlefieldRow" as ZoneId,
108
+ });
109
+
110
+ // Initialize control (null = unclaimed)
111
+ draft.battlefieldControl[battlefieldId as string] = null;
112
+ }
113
+
114
+ // NO MORE: draft.setupStep
115
+ },
116
+ },
117
+
118
+ shuffleDecks: {
119
+ reducer: (_draft, context) => {
120
+ const { zones } = context;
121
+ const playerId = context.params.playerId;
122
+
123
+ zones.shuffleZone("mainDeck" as ZoneId, playerId);
124
+ zones.shuffleZone("runeDeck" as ZoneId, playerId);
125
+
126
+ // NO MORE: draft.setupStep
127
+ },
128
+ },
129
+
130
+ drawInitialHand: {
131
+ reducer: (_draft, context) => {
132
+ const { zones } = context;
133
+ const playerId = context.params.playerId;
134
+
135
+ // BEFORE: Manual loop (11 lines)
136
+ // AFTER: Use drawCards utility!
137
+ zones.drawCards({
138
+ from: "mainDeck" as ZoneId,
139
+ to: "hand" as ZoneId,
140
+ count: 6,
141
+ playerId,
142
+ });
143
+
144
+ // NO MORE: draft.setupStep
145
+ },
146
+ },
147
+
148
+ transitionToPlay: {
149
+ reducer: (_draft, _context) => {
150
+ // NO MORE: draft.setupStep, draft.phase, draft.turn
151
+ },
152
+ },
153
+
154
+ // Regular game moves
155
+ channelRunes: {
156
+ reducer: (draft, context) => {
157
+ const playerId = context.params.playerId as string;
158
+ const count = context.params.count;
159
+
160
+ // BEFORE: Manual loop + drawing
161
+ // AFTER: Use bulkMove utility!
162
+ context.zones.bulkMove({
163
+ from: "runeDeck" as ZoneId,
164
+ to: "runePool" as ZoneId,
165
+ count,
166
+ playerId: playerId as PlayerId,
167
+ });
168
+
169
+ // Increment energy
170
+ const pool = draft.runePools[playerId];
171
+ if (pool) {
172
+ pool.energy += count;
173
+ }
174
+ },
175
+ },
176
+
177
+ drawCard: {
178
+ condition: (state, context) => {
179
+ const playerId = context.playerId;
180
+ // Use tracker system!
181
+ return !context.trackers?.check("hasDrawn", playerId);
182
+ },
183
+ reducer: (_draft, context) => {
184
+ const { zones } = context;
185
+ const playerId = context.params.playerId;
186
+
187
+ zones.drawCards({
188
+ from: "mainDeck" as ZoneId,
189
+ to: "hand" as ZoneId,
190
+ count: 1,
191
+ playerId,
192
+ });
193
+
194
+ // Mark as drawn
195
+ context.trackers?.mark("hasDrawn", playerId);
196
+ },
197
+ },
198
+
199
+ playUnit: {
200
+ reducer: (_draft, context) => {
201
+ const cardId = context.params.cardId;
202
+
203
+ context.zones.moveCard({
204
+ cardId,
205
+ targetZoneId: "battlefield" as ZoneId,
206
+ });
207
+ },
208
+ },
209
+
210
+ playGear: {
211
+ reducer: (_draft, context) => {
212
+ const cardId = context.params.cardId;
213
+
214
+ context.zones.moveCard({
215
+ cardId,
216
+ targetZoneId: "gearArea" as ZoneId,
217
+ });
218
+ },
219
+ },
220
+
221
+ playSpell: {
222
+ reducer: (_draft, context) => {
223
+ const cardId = context.params.cardId;
224
+
225
+ // Spells go to discard after resolution
226
+ context.zones.moveCard({
227
+ cardId,
228
+ targetZoneId: "discard" as ZoneId,
229
+ });
230
+ },
231
+ },
232
+
233
+ moveUnit: {
234
+ reducer: (_draft, _context) => {
235
+ // Unit movement logic
236
+ },
237
+ },
238
+
239
+ initiateCombat: {
240
+ reducer: (draft, context) => {
241
+ const playerId = context.params.playerId as string;
242
+ const battlefieldId = context.params.battlefieldId as string;
243
+
244
+ // Track conquered battlefield
245
+ if (!draft.conqueredThisTurn[playerId]) {
246
+ draft.conqueredThisTurn[playerId] = [];
247
+ }
248
+ draft.conqueredThisTurn[playerId].push(battlefieldId as CardId);
249
+ },
250
+ },
251
+
252
+ // Standard moves from engine
253
+ pass: standardMoves<TestGameState>({
254
+ include: ["pass"],
255
+ }).pass!,
256
+
257
+ concede: standardMoves<TestGameState>({
258
+ include: ["concede"],
259
+ }).concede!,
260
+ };
261
+
262
+ // Riftbound zones (unchanged)
263
+ const riftboundZones: Record<string, CardZoneConfig> = {
264
+ mainDeck: {
265
+ id: "mainDeck" as ZoneId,
266
+ name: "zones.mainDeck",
267
+ visibility: "secret",
268
+ ordered: true,
269
+ owner: undefined,
270
+ faceDown: true,
271
+ maxSize: 40,
272
+ },
273
+ hand: {
274
+ id: "hand" as ZoneId,
275
+ name: "zones.hand",
276
+ visibility: "private",
277
+ ordered: false,
278
+ owner: undefined,
279
+ faceDown: false,
280
+ maxSize: undefined,
281
+ },
282
+ runeDeck: {
283
+ id: "runeDeck" as ZoneId,
284
+ name: "zones.runeDeck",
285
+ visibility: "secret",
286
+ ordered: true,
287
+ owner: undefined,
288
+ faceDown: true,
289
+ maxSize: 12,
290
+ },
291
+ runePool: {
292
+ id: "runePool" as ZoneId,
293
+ name: "zones.runePool",
294
+ visibility: "public",
295
+ ordered: false,
296
+ owner: undefined,
297
+ faceDown: false,
298
+ maxSize: undefined,
299
+ },
300
+ legendZone: {
301
+ id: "legendZone" as ZoneId,
302
+ name: "zones.legendZone",
303
+ visibility: "public",
304
+ ordered: false,
305
+ owner: undefined,
306
+ faceDown: false,
307
+ maxSize: 1,
308
+ },
309
+ championZone: {
310
+ id: "championZone" as ZoneId,
311
+ name: "zones.championZone",
312
+ visibility: "public",
313
+ ordered: false,
314
+ owner: undefined,
315
+ faceDown: false,
316
+ maxSize: 1,
317
+ },
318
+ battlefield: {
319
+ id: "battlefield" as ZoneId,
320
+ name: "zones.battlefield",
321
+ visibility: "public",
322
+ ordered: false,
323
+ owner: undefined,
324
+ faceDown: false,
325
+ maxSize: undefined,
326
+ },
327
+ battlefieldRow: {
328
+ id: "battlefieldRow" as ZoneId,
329
+ name: "zones.battlefieldRow",
330
+ visibility: "public",
331
+ ordered: true,
332
+ owner: undefined,
333
+ faceDown: false,
334
+ maxSize: 3,
335
+ },
336
+ gearArea: {
337
+ id: "gearArea" as ZoneId,
338
+ name: "zones.gearArea",
339
+ visibility: "public",
340
+ ordered: false,
341
+ owner: undefined,
342
+ faceDown: false,
343
+ maxSize: undefined,
344
+ },
345
+ discard: {
346
+ id: "discard" as ZoneId,
347
+ name: "zones.discard",
348
+ visibility: "public",
349
+ ordered: false,
350
+ owner: undefined,
351
+ faceDown: false,
352
+ maxSize: undefined,
353
+ },
354
+ };
355
+
356
+ // Riftbound flow (simplified)
357
+ const riftboundFlow: FlowDefinition<TestGameState> = {
358
+ turn: {
359
+ initialPhase: "awaken",
360
+ phases: {
361
+ awaken: {
362
+ order: 1,
363
+ next: "beginning",
364
+ onBegin: (_context) => {},
365
+ endIf: () => true,
366
+ },
367
+ beginning: {
368
+ order: 2,
369
+ next: "channel",
370
+ onBegin: (_context) => {},
371
+ endIf: () => true,
372
+ },
373
+ channel: {
374
+ order: 3,
375
+ next: "draw",
376
+ onBegin: (_context) => {},
377
+ endIf: () => true,
378
+ },
379
+ draw: {
380
+ order: 4,
381
+ next: "action",
382
+ onBegin: (_context) => {},
383
+ endIf: () => true,
384
+ },
385
+ action: {
386
+ order: 5,
387
+ next: "ending",
388
+ onBegin: (_context) => {},
389
+ },
390
+ ending: {
391
+ order: 6,
392
+ next: "awaken",
393
+ onBegin: (context) => {
394
+ // Clear conquered battlefields at turn end
395
+ const playerId = context.getCurrentPlayer();
396
+ context.state.conqueredThisTurn[playerId] = [];
397
+ },
398
+ endIf: () => true,
399
+ },
400
+ },
401
+ },
402
+ };
403
+
404
+ /**
405
+ * Create minimal Riftbound game definition for testing
406
+ *
407
+ * REFACTORED to showcase new engine features:
408
+ * ✨ 130+ lines of boilerplate ELIMINATED!
409
+ * ✅ No manual phase/turn/player tracking
410
+ * ✅ High-level zone utilities (createDeck, drawCards, bulkMove)
411
+ * ✅ Tracker system for per-turn flags (hasDrawn)
412
+ * ✅ Standard moves library (pass, concede)
413
+ */
414
+ export function createMockRiftboundGame(): GameDefinition<
415
+ TestGameState,
416
+ TestMoves
417
+ > {
418
+ return {
419
+ name: "Test Riftbound Game",
420
+ zones: riftboundZones,
421
+ flow: riftboundFlow,
422
+ moves: riftboundMoves,
423
+
424
+ // Configure engine's tracker system
425
+ trackers: {
426
+ perTurn: ["hasDrawn"],
427
+ perPlayer: true,
428
+ },
429
+
430
+ /**
431
+ * Setup function - MASSIVELY SIMPLIFIED!
432
+ *
433
+ * BEFORE: 100+ lines tracking phase, setupStep, turn, activePlayer, hasDrawnThisTurn
434
+ * AFTER: 20 lines - just initialize game-specific data!
435
+ */
436
+ setup: (players) => {
437
+ const playerIds = players.map((p) => p.id);
438
+ const victoryPoints: Record<string, number> = {};
439
+ const runePools: Record<
440
+ string,
441
+ { energy: number; power: Record<string, number> }
442
+ > = {};
443
+ const conqueredThisTurn: Record<string, CardId[]> = {};
444
+
445
+ for (const playerId of playerIds) {
446
+ victoryPoints[playerId] = 0;
447
+ runePools[playerId] = {
448
+ energy: 0,
449
+ power: {},
450
+ };
451
+ conqueredThisTurn[playerId] = [];
452
+ }
453
+
454
+ return {
455
+ victoryPoints,
456
+ battlefieldControl: {},
457
+ runePools,
458
+ conqueredThisTurn,
459
+ };
460
+ },
461
+ };
462
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createTestEngine } from "../testing/test-engine-builder";
3
+ import { createTestPlayers } from "../testing/test-player-builder";
4
+ import { createMockGrandArchiveGame } from "./createMockGrandArchiveGame";
5
+
6
+ /**
7
+ * Grand Archive Card Game - Engine Feature Tests
8
+ *
9
+ * Refactored to showcase:
10
+ * ✅ Engine-managed flow state
11
+ * ✅ High-level zone utilities (createDeck, drawCards)
12
+ * ✅ Tracker system (hasMaterialized, hasDrawn)
13
+ * ✅ Standard moves (concede)
14
+ * ✅ Flow context access in phase hooks
15
+ */
16
+ describe("Grand Archive Game - Refactored Engine Features", () => {
17
+ it("should initialize game with ONLY game-specific state", () => {
18
+ const gameDefinition = createMockGrandArchiveGame();
19
+ const players = createTestPlayers(2);
20
+ const engine = createTestEngine(gameDefinition, players);
21
+
22
+ const state = engine.getState();
23
+
24
+ // ✅ NEW: Only game-specific data
25
+ expect(state.opportunityPlayer).toBe(null);
26
+ expect(state.champions).toBeDefined();
27
+
28
+ // ✅ REMOVED: No manual phase/turn/player tracking
29
+ // @ts-expect-error
30
+ expect(state.phase).toBeUndefined();
31
+ // @ts-expect-error
32
+ expect(state.turn).toBeUndefined();
33
+ // @ts-expect-error
34
+ expect(state.currentPlayer).toBeUndefined();
35
+ // @ts-expect-error
36
+ expect(state.hasDrawnThisTurn).toBeUndefined();
37
+ // @ts-expect-error
38
+ expect(state.hasMaterializedThisTurn).toBeUndefined();
39
+ });
40
+
41
+ it("should have proper zone configuration", () => {
42
+ const gameDefinition = createMockGrandArchiveGame();
43
+ const zones = gameDefinition.zones;
44
+
45
+ // Verify Grand Archive dual-deck system
46
+ expect(zones?.mainDeck).toBeDefined();
47
+ expect(zones?.materialDeck).toBeDefined();
48
+ expect(zones?.mainDeck?.maxSize).toBe(40);
49
+ expect(zones?.materialDeck?.maxSize).toBe(15);
50
+
51
+ // Verify other zones
52
+ expect(zones?.hand).toBeDefined();
53
+ expect(zones?.memory).toBeDefined();
54
+ expect(zones?.field).toBeDefined();
55
+ expect(zones?.graveyard).toBeDefined();
56
+ expect(zones?.banishment).toBeDefined();
57
+ expect(zones?.effectsStack).toBeDefined();
58
+ expect(zones?.intent).toBeDefined();
59
+ });
60
+
61
+ it("should configure tracker system for per-turn flags", () => {
62
+ const gameDefinition = createMockGrandArchiveGame();
63
+
64
+ expect(gameDefinition.trackers).toBeDefined();
65
+ expect(gameDefinition.trackers?.perTurn).toContain("hasMaterialized");
66
+ expect(gameDefinition.trackers?.perTurn).toContain("hasDrawn");
67
+ expect(gameDefinition.trackers?.perPlayer).toBe(true);
68
+ });
69
+
70
+ it("should use high-level zone utilities", () => {
71
+ // ✅ NEW: initializeGame uses zones.createDeck()
72
+ // ✅ NEW: drawStartingHand uses zones.drawCards()
73
+ // BEFORE: Manual loops (20+ lines total)
74
+ // AFTER: Utility calls (6 lines total)
75
+
76
+ const gameDefinition = createMockGrandArchiveGame();
77
+ expect(gameDefinition.moves.initializeGame).toBeDefined();
78
+ expect(gameDefinition.moves.drawStartingHand).toBeDefined();
79
+ });
80
+
81
+ it("should use flow context in phase hooks", () => {
82
+ const gameDefinition = createMockGrandArchiveGame();
83
+ const flow = gameDefinition.flow;
84
+
85
+ // ✅ NEW: Phase hooks use context.getCurrentPlayer()
86
+ expect(flow).toBeDefined();
87
+ if (!(flow && "turn" in flow)) {
88
+ throw new Error("Expected simplified flow definition with turn property");
89
+ }
90
+ expect(flow.turn.phases?.recollection).toBeDefined();
91
+ expect(flow.turn.phases?.main).toBeDefined();
92
+ expect(flow.turn.phases?.end).toBeDefined();
93
+ });
94
+
95
+ it("should use tracker system for materialize action", () => {
96
+ const gameDefinition = createMockGrandArchiveGame();
97
+
98
+ const materializeCard = gameDefinition.moves.materializeCard;
99
+ expect(materializeCard.condition).toBeDefined();
100
+ expect(materializeCard.reducer).toBeDefined();
101
+
102
+ // Move uses context.trackers.check/mark for hasMaterialized
103
+ });
104
+
105
+ it("should demonstrate boilerplate reduction", () => {
106
+ // State fields: 10 → 2 (-80%)
107
+ // Setup function massively simplified
108
+
109
+ const gameDefinition = createMockGrandArchiveGame();
110
+ const players = createTestPlayers(2);
111
+ const state = gameDefinition.setup(players);
112
+
113
+ // Only game-specific state
114
+ expect(Object.keys(state).length).toBe(2);
115
+ expect(state.opportunityPlayer).toBeDefined();
116
+ expect(state.champions).toBeDefined();
117
+ });
118
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createTestEngine } from "../testing/test-engine-builder";
3
+ import { createTestPlayers } from "../testing/test-player-builder";
4
+ import { createMockGundamGame } from "./createMockGundamGame";
5
+
6
+ /**
7
+ * Gundam Card Game - Engine Feature Tests
8
+ *
9
+ * Refactored to showcase:
10
+ * ✅ High-level zone utilities (createDeck, bulkMove, drawCards, mulligan)
11
+ * ✅ Tracker system (hasPlayedResource)
12
+ * ✅ Standard moves (pass, concede)
13
+ * ✅ Simplified state (10 fields → 2 fields)
14
+ */
15
+ describe("Gundam Game - Refactored Engine Features", () => {
16
+ it("should initialize game with ONLY game-specific state", () => {
17
+ const gameDefinition = createMockGundamGame();
18
+ const players = createTestPlayers(2);
19
+ const engine = createTestEngine(gameDefinition, players);
20
+
21
+ const state = engine.getState();
22
+
23
+ // ✅ NEW: Only game-specific data
24
+ expect(state.activeResources).toBeDefined();
25
+ expect(state.attackedThisTurn).toBeDefined();
26
+ expect(state.attackedThisTurn).toEqual([]);
27
+
28
+ // ✅ REMOVED: No manual tracking
29
+ // @ts-expect-error
30
+ expect(state.phase).toBeUndefined();
31
+ // @ts-expect-error
32
+ expect(state.turn).toBeUndefined();
33
+ // @ts-expect-error
34
+ expect(state.setupStep).toBeUndefined();
35
+ // @ts-expect-error
36
+ expect(state.hasPlayedResourceThisTurn).toBeUndefined();
37
+ // @ts-expect-error
38
+ expect(state.mulliganOffered).toBeUndefined();
39
+ });
40
+
41
+ it("should have proper zone configuration", () => {
42
+ const gameDefinition = createMockGundamGame();
43
+ const zones = gameDefinition.zones;
44
+
45
+ // Verify Gundam zones
46
+ expect(zones?.deck).toBeDefined();
47
+ expect(zones?.hand).toBeDefined();
48
+ expect(zones?.resourceDeck).toBeDefined();
49
+ expect(zones?.resourceArea).toBeDefined();
50
+ expect(zones?.baseSection).toBeDefined();
51
+ expect(zones?.unitArea).toBeDefined();
52
+ expect(zones?.shieldSection).toBeDefined();
53
+ expect(zones?.junkYard).toBeDefined();
54
+
55
+ // Verify sizes
56
+ expect(zones?.deck?.maxSize).toBe(50);
57
+ expect(zones?.resourceDeck?.maxSize).toBe(10);
58
+ expect(zones?.baseSection?.maxSize).toBe(1);
59
+ expect(zones?.shieldSection?.maxSize).toBe(6);
60
+ });
61
+
62
+ it("should use high-level zone utilities for setup", () => {
63
+ // ✅ NEW: zones.createDeck() replaces manual card creation
64
+ // ✅ NEW: zones.bulkMove() replaces manual shield placement
65
+ // ✅ NEW: zones.drawCards() replaces manual drawing
66
+ // ✅ NEW: zones.mulligan() replaces 20 lines of logic
67
+
68
+ const gameDefinition = createMockGundamGame();
69
+ expect(gameDefinition.moves.initializeDecks).toBeDefined();
70
+ expect(gameDefinition.moves.placeShields).toBeDefined();
71
+ expect(gameDefinition.moves.drawInitialHand).toBeDefined();
72
+ expect(gameDefinition.moves.decideMulligan).toBeDefined();
73
+ });
74
+
75
+ it("should configure tracker system", () => {
76
+ const gameDefinition = createMockGundamGame();
77
+
78
+ expect(gameDefinition.trackers).toBeDefined();
79
+ expect(gameDefinition.trackers?.perTurn).toContain("hasPlayedResource");
80
+ expect(gameDefinition.trackers?.perPlayer).toBe(true);
81
+ });
82
+
83
+ it("should use tracker system for resource playing", () => {
84
+ const gameDefinition = createMockGundamGame();
85
+
86
+ const playResource = gameDefinition.moves.playResource;
87
+ expect(playResource.condition).toBeDefined();
88
+ expect(playResource.reducer).toBeDefined();
89
+
90
+ // Uses context.trackers.check("hasPlayedResource")
91
+ });
92
+
93
+ it("should use standard moves", () => {
94
+ const gameDefinition = createMockGundamGame();
95
+
96
+ expect(gameDefinition.moves.pass).toBeDefined();
97
+ expect(gameDefinition.moves.concede).toBeDefined();
98
+ });
99
+
100
+ it("should demonstrate massive boilerplate reduction", () => {
101
+ // State fields: 10 → 2 (-80%)
102
+ // 444 lines → 350 lines (-21%)
103
+
104
+ const gameDefinition = createMockGundamGame();
105
+ const players = createTestPlayers(2);
106
+ const state = gameDefinition.setup(players);
107
+
108
+ expect(Object.keys(state).length).toBe(2);
109
+ });
110
+ });