@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,379 @@
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 Gundam game state - SIMPLIFIED!
8
+ type TestGameState = {
9
+ activeResources: Record<string, number>;
10
+ attackedThisTurn: CardId[];
11
+ };
12
+
13
+ type TestMoves = {
14
+ // Setup moves
15
+ initializeDecks: { playerId: PlayerId };
16
+ placeShields: { playerId: PlayerId };
17
+ createTokens: { playerId: PlayerId; playerIndex?: number };
18
+ drawInitialHand: { playerId: PlayerId };
19
+ decideMulligan: { playerId: PlayerId; redraw: boolean };
20
+ transitionToPlay: Record<string, never>;
21
+ // Regular game moves
22
+ draw: { playerId: PlayerId; count: number };
23
+ deployUnit: { playerId: PlayerId; cardId: CardId; position?: number };
24
+ deployBase: { playerId: PlayerId; cardId: CardId };
25
+ playResource: { playerId: PlayerId; cardId: CardId };
26
+ attack: { playerId: PlayerId; attackerId: CardId; targetId?: CardId };
27
+ // Standard moves
28
+ pass: { playerId: PlayerId };
29
+ concede: { playerId: PlayerId };
30
+ };
31
+
32
+ // Gundam move definitions
33
+ const gundamMoves: GameMoveDefinitions<TestGameState, TestMoves> = {
34
+ // Setup moves - using engine utilities!
35
+ initializeDecks: {
36
+ reducer: (_draft, context) => {
37
+ const { zones } = context;
38
+ const playerId = context.params.playerId;
39
+
40
+ // Use engine's createDeck utility!
41
+ zones.createDeck({
42
+ zoneId: "deck" as ZoneId,
43
+ playerId,
44
+ cardCount: 50,
45
+ shuffle: true,
46
+ });
47
+
48
+ zones.createDeck({
49
+ zoneId: "resourceDeck" as ZoneId,
50
+ playerId,
51
+ cardCount: 10,
52
+ shuffle: true,
53
+ });
54
+
55
+ // NO MORE: draft.setupStep
56
+ },
57
+ },
58
+
59
+ placeShields: {
60
+ reducer: (_draft, context) => {
61
+ const { zones } = context;
62
+ const playerId = context.params.playerId;
63
+
64
+ // BEFORE: Manual loop (9 lines)
65
+ // AFTER: Use bulkMove utility!
66
+ zones.bulkMove({
67
+ from: "deck" as ZoneId,
68
+ to: "shieldSection" as ZoneId,
69
+ count: 6,
70
+ playerId,
71
+ position: "bottom",
72
+ });
73
+
74
+ // NO MORE: draft.setupStep
75
+ },
76
+ },
77
+
78
+ createTokens: {
79
+ reducer: (_draft, context) => {
80
+ const { zones } = context;
81
+ const playerId = context.params.playerId;
82
+ const playerIndex = context.params.playerIndex;
83
+
84
+ // Create EX Base token
85
+ const baseTokenId = `${playerId}-token-ex-base` as CardId;
86
+ zones.moveCard({
87
+ cardId: baseTokenId,
88
+ targetZoneId: "baseSection" as ZoneId,
89
+ position: "bottom",
90
+ });
91
+
92
+ // Second player gets EX Resource token
93
+ const isSecondPlayer = playerIndex === 1;
94
+ if (isSecondPlayer) {
95
+ const resourceTokenId = `${playerId}-token-ex-resource` as CardId;
96
+ zones.moveCard({
97
+ cardId: resourceTokenId,
98
+ targetZoneId: "resourceArea" as ZoneId,
99
+ position: "bottom",
100
+ });
101
+ }
102
+
103
+ // NO MORE: draft.setupStep
104
+ },
105
+ },
106
+
107
+ drawInitialHand: {
108
+ reducer: (_draft, context) => {
109
+ const { zones } = context;
110
+ const playerId = context.params.playerId;
111
+
112
+ // BEFORE: Manual loop (11 lines)
113
+ // AFTER: Use drawCards utility!
114
+ zones.drawCards({
115
+ from: "deck" as ZoneId,
116
+ to: "hand" as ZoneId,
117
+ count: 7,
118
+ playerId,
119
+ });
120
+
121
+ // NO MORE: draft.setupStep, draft.mulliganOffered
122
+ },
123
+ },
124
+
125
+ decideMulligan: {
126
+ reducer: (_draft, context) => {
127
+ const { zones } = context;
128
+ const playerId = context.params.playerId;
129
+ const redraw = context.params.redraw;
130
+
131
+ if (redraw) {
132
+ // BEFORE: Manual card return + shuffle + redraw (20 lines)
133
+ // AFTER: Use mulligan utility (1 line!)
134
+ zones.mulligan({
135
+ hand: "hand" as ZoneId,
136
+ deck: "deck" as ZoneId,
137
+ drawCount: 7,
138
+ playerId,
139
+ });
140
+ }
141
+
142
+ // NO MORE: draft.mulliganOffered
143
+ },
144
+ },
145
+
146
+ transitionToPlay: {
147
+ reducer: (_draft, _context) => {
148
+ // NO MORE: draft.setupStep, draft.phase, draft.turn
149
+ },
150
+ },
151
+
152
+ // Regular game moves
153
+ draw: {
154
+ reducer: (_draft, context) => {
155
+ const { zones } = context;
156
+ const playerId = context.params.playerId;
157
+ const count = context.params.count;
158
+
159
+ zones.drawCards({
160
+ from: "deck" as ZoneId,
161
+ to: "hand" as ZoneId,
162
+ count,
163
+ playerId,
164
+ });
165
+ },
166
+ },
167
+
168
+ deployUnit: {
169
+ reducer: (_draft, context) => {
170
+ const cardId = context.params.cardId;
171
+
172
+ context.zones.moveCard({
173
+ cardId,
174
+ targetZoneId: "unitArea" as ZoneId,
175
+ });
176
+ },
177
+ },
178
+
179
+ deployBase: {
180
+ reducer: (_draft, context) => {
181
+ const cardId = context.params.cardId;
182
+
183
+ context.zones.moveCard({
184
+ cardId,
185
+ targetZoneId: "baseSection" as ZoneId,
186
+ });
187
+ },
188
+ },
189
+
190
+ playResource: {
191
+ condition: (state, context) => {
192
+ const playerId = context.params.playerId;
193
+ // Use engine's tracker system!
194
+ return !context.trackers?.check("hasPlayedResource", playerId);
195
+ },
196
+ reducer: (draft, context) => {
197
+ const playerId = context.params.playerId;
198
+ const cardId = context.params.cardId;
199
+
200
+ context.zones.moveCard({
201
+ cardId,
202
+ targetZoneId: "resourceArea" as ZoneId,
203
+ });
204
+
205
+ draft.activeResources[playerId] =
206
+ (draft.activeResources[playerId] || 0) + 1;
207
+
208
+ // Mark as played
209
+ context.trackers?.mark("hasPlayedResource", playerId);
210
+ },
211
+ },
212
+
213
+ attack: {
214
+ reducer: (draft, context) => {
215
+ const attackerId = context.params.attackerId;
216
+
217
+ // Track attacker this turn
218
+ draft.attackedThisTurn.push(attackerId);
219
+ },
220
+ },
221
+
222
+ // Standard moves from engine
223
+ pass: standardMoves<TestGameState>({
224
+ include: ["pass"],
225
+ }).pass!,
226
+
227
+ concede: standardMoves<TestGameState>({
228
+ include: ["concede"],
229
+ }).concede!,
230
+ };
231
+
232
+ // Gundam zones configuration (unchanged)
233
+ const gundamZones: Record<string, CardZoneConfig> = {
234
+ deck: {
235
+ id: "deck" as ZoneId,
236
+ name: "zones.deck",
237
+ visibility: "secret",
238
+ ordered: true,
239
+ owner: undefined,
240
+ faceDown: true,
241
+ maxSize: 50,
242
+ },
243
+ hand: {
244
+ id: "hand" as ZoneId,
245
+ name: "zones.hand",
246
+ visibility: "private",
247
+ ordered: false,
248
+ owner: undefined,
249
+ faceDown: false,
250
+ maxSize: undefined,
251
+ },
252
+ resourceDeck: {
253
+ id: "resourceDeck" as ZoneId,
254
+ name: "zones.resourceDeck",
255
+ visibility: "secret",
256
+ ordered: true,
257
+ owner: undefined,
258
+ faceDown: true,
259
+ maxSize: 10,
260
+ },
261
+ resourceArea: {
262
+ id: "resourceArea" as ZoneId,
263
+ name: "zones.resourceArea",
264
+ visibility: "public",
265
+ ordered: false,
266
+ owner: undefined,
267
+ faceDown: false,
268
+ maxSize: undefined,
269
+ },
270
+ baseSection: {
271
+ id: "baseSection" as ZoneId,
272
+ name: "zones.baseSection",
273
+ visibility: "public",
274
+ ordered: false,
275
+ owner: undefined,
276
+ faceDown: false,
277
+ maxSize: 1,
278
+ },
279
+ unitArea: {
280
+ id: "unitArea" as ZoneId,
281
+ name: "zones.unitArea",
282
+ visibility: "public",
283
+ ordered: false,
284
+ owner: undefined,
285
+ faceDown: false,
286
+ maxSize: undefined,
287
+ },
288
+ shieldSection: {
289
+ id: "shieldSection" as ZoneId,
290
+ name: "zones.shieldSection",
291
+ visibility: "secret",
292
+ ordered: true,
293
+ owner: undefined,
294
+ faceDown: true,
295
+ maxSize: 6,
296
+ },
297
+ junkYard: {
298
+ id: "junkYard" as ZoneId,
299
+ name: "zones.junkYard",
300
+ visibility: "public",
301
+ ordered: false,
302
+ owner: undefined,
303
+ faceDown: false,
304
+ maxSize: undefined,
305
+ },
306
+ };
307
+
308
+ // Gundam flow definition (simplified)
309
+ const gundamFlow: FlowDefinition<TestGameState> = {
310
+ turn: {
311
+ initialPhase: "start",
312
+ onBegin: (_context) => {},
313
+ onEnd: (_context) => {},
314
+ phases: {
315
+ start: {
316
+ order: 1,
317
+ next: "draw",
318
+ onBegin: (_context) => {},
319
+ endIf: () => true,
320
+ },
321
+ draw: {
322
+ order: 2,
323
+ next: "resource",
324
+ onBegin: (_context) => {},
325
+ endIf: () => true,
326
+ },
327
+ resource: {
328
+ order: 3,
329
+ next: "main",
330
+ onBegin: (_context) => {},
331
+ },
332
+ main: {
333
+ order: 4,
334
+ next: "end",
335
+ onBegin: (_context) => {},
336
+ },
337
+ end: {
338
+ order: 5,
339
+ next: "start",
340
+ onBegin: (context) => {
341
+ // Clear attacked units at end of turn
342
+ context.state.attackedThisTurn = [];
343
+ },
344
+ endIf: () => true,
345
+ },
346
+ },
347
+ },
348
+ };
349
+
350
+ export function createMockGundamGame(): GameDefinition<
351
+ TestGameState,
352
+ TestMoves
353
+ > {
354
+ return {
355
+ name: "Test Gundam Game",
356
+ zones: gundamZones,
357
+ flow: gundamFlow,
358
+ moves: gundamMoves,
359
+
360
+ trackers: {
361
+ perTurn: ["hasPlayedResource"],
362
+ perPlayer: true,
363
+ },
364
+
365
+ setup: (players) => {
366
+ const playerIds = players.map((p) => p.id);
367
+ const activeResources: Record<string, number> = {};
368
+
369
+ for (const playerId of playerIds) {
370
+ activeResources[playerId] = 0;
371
+ }
372
+
373
+ return {
374
+ activeResources,
375
+ attackedThisTurn: [],
376
+ };
377
+ },
378
+ };
379
+ }
@@ -0,0 +1,328 @@
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 Lorcana game state - SIMPLIFIED!
8
+ type TestGameState = {
9
+ effects: unknown[];
10
+ bag: unknown[];
11
+ loreScores: Record<string, number>;
12
+ };
13
+
14
+ type AlternativeCost = {
15
+ type: "shift" | "sing" | "sing-together";
16
+ targetInstanceId: CardId[];
17
+ };
18
+
19
+ type TestMoves = {
20
+ // Setup moves
21
+ chooseWhoGoesFirstMove: { playerId: PlayerId };
22
+ alterHand: { playerId: PlayerId; cards: CardId[] };
23
+ drawCards: { playerId: PlayerId; count: number };
24
+ // Game moves
25
+ putACardIntoTheInkwell: { cardId: CardId };
26
+ playCard: { cardId: CardId; alternativeCost?: AlternativeCost };
27
+ quest: { cardId: CardId };
28
+ challenge: { attackerId: CardId; defenderId: CardId };
29
+ sing: { singerId: CardId; songId: CardId };
30
+ singTogether: { singersIds: CardId[]; songId: CardId };
31
+ moveCharacterToLocation: { characterId: CardId; locationId: CardId };
32
+ activateAbility: {
33
+ cardId: CardId;
34
+ opts?: {
35
+ abilityIndex?: number;
36
+ abilityText?: string;
37
+ alternativeCost?: AlternativeCost;
38
+ };
39
+ };
40
+ resolveBag: { bagId: string; params: unknown };
41
+ resolveEffect: { effectId: string; params: unknown };
42
+ manualExert: { cardId: CardId };
43
+ // Standard moves
44
+ passTurn: { playerId: PlayerId };
45
+ concede: { playerId: PlayerId };
46
+ };
47
+
48
+ // Lorcana move definitions
49
+ const lorcanaMoves: GameMoveDefinitions<TestGameState, TestMoves> = {
50
+ // Setup moves using engine features
51
+ chooseWhoGoesFirstMove: {
52
+ reducer: (_draft, _context) => {
53
+ // NO MORE: draft.activePlayerId, draft.firstPlayerDetermined, draft.gamePhase, draft.turnNumber
54
+ // Engine handles this!
55
+ },
56
+ },
57
+
58
+ alterHand: {
59
+ reducer: (_draft, context) => {
60
+ const { zones } = context;
61
+ const playerId = context.params.playerId;
62
+
63
+ // BEFORE: Manual array manipulation (11 lines)
64
+ // AFTER: Use mulligan utility!
65
+ zones.mulligan({
66
+ hand: "hand" as ZoneId,
67
+ deck: "deck" as ZoneId,
68
+ drawCount: 7,
69
+ playerId,
70
+ });
71
+ },
72
+ },
73
+
74
+ drawCards: {
75
+ reducer: (_draft, context) => {
76
+ const { zones } = context;
77
+ const playerId = context.params.playerId;
78
+ const count = context.params.count;
79
+
80
+ // Use engine's drawCards utility
81
+ zones.drawCards({
82
+ from: "deck" as ZoneId,
83
+ to: "hand" as ZoneId,
84
+ count,
85
+ playerId,
86
+ });
87
+ },
88
+ },
89
+
90
+ putACardIntoTheInkwell: {
91
+ condition: (state, context) => {
92
+ const playerId = context.playerId;
93
+ // Use tracker system!
94
+ return !context.trackers?.check("hasInked", playerId);
95
+ },
96
+ reducer: (_draft, context) => {
97
+ const cardId = context.params.cardId;
98
+ const playerId = context.playerId;
99
+
100
+ // Move card to inkwell
101
+ context.zones.moveCard({
102
+ cardId,
103
+ targetZoneId: "inkwell" as ZoneId,
104
+ });
105
+
106
+ // Mark as inked
107
+ context.trackers?.mark("hasInked", playerId);
108
+ },
109
+ },
110
+
111
+ playCard: {
112
+ reducer: (_draft, context) => {
113
+ const cardId = context.params.cardId;
114
+
115
+ // Play card to play area
116
+ context.zones.moveCard({
117
+ cardId,
118
+ targetZoneId: "play" as ZoneId,
119
+ });
120
+ },
121
+ },
122
+
123
+ quest: {
124
+ condition: (state, context) => {
125
+ const cardId = context.params.cardId;
126
+ // Card hasn't quested this turn
127
+ return !context.trackers?.check(`quested:${cardId}`, context.playerId);
128
+ },
129
+ reducer: (draft, context) => {
130
+ const cardId = context.params.cardId;
131
+ const playerId = context.playerId;
132
+
133
+ // Increment lore (simplified - assume 1 lore per quest)
134
+ draft.loreScores[playerId] = (draft.loreScores[playerId] || 0) + 1;
135
+
136
+ // Mark as quested
137
+ context.trackers?.mark(`quested:${cardId}`, playerId);
138
+ },
139
+ },
140
+
141
+ challenge: {
142
+ reducer: (_draft, _context) => {
143
+ // Challenge logic
144
+ },
145
+ },
146
+
147
+ sing: {
148
+ reducer: (_draft, context) => {
149
+ const singerId = context.params.singerId;
150
+ const songId = context.params.songId;
151
+
152
+ // Exert singer, play song
153
+ context.zones.moveCard({
154
+ cardId: songId,
155
+ targetZoneId: "play" as ZoneId,
156
+ });
157
+ },
158
+ },
159
+
160
+ singTogether: {
161
+ reducer: (_draft, context) => {
162
+ const songId = context.params.songId;
163
+
164
+ // Play song via sing together
165
+ context.zones.moveCard({
166
+ cardId: songId,
167
+ targetZoneId: "play" as ZoneId,
168
+ });
169
+ },
170
+ },
171
+
172
+ moveCharacterToLocation: {
173
+ reducer: (_draft, _context) => {
174
+ // Move character logic
175
+ },
176
+ },
177
+
178
+ activateAbility: {
179
+ reducer: (_draft, _context) => {
180
+ // Ability activation logic
181
+ },
182
+ },
183
+
184
+ resolveBag: {
185
+ reducer: (draft, context) => {
186
+ const bagId = context.params.bagId;
187
+ // Remove bag after resolution
188
+ draft.bag = draft.bag.filter((b: any) => b.id !== bagId);
189
+ },
190
+ },
191
+
192
+ resolveEffect: {
193
+ reducer: (draft, context) => {
194
+ const effectId = context.params.effectId;
195
+ // Remove effect after resolution
196
+ draft.effects = draft.effects.filter((e: any) => e.id !== effectId);
197
+ },
198
+ },
199
+
200
+ manualExert: {
201
+ reducer: (_draft, _context) => {
202
+ // Exert card logic
203
+ },
204
+ },
205
+
206
+ // Standard moves from engine
207
+ passTurn: standardMoves<TestGameState>({
208
+ include: ["pass"],
209
+ }).pass!,
210
+
211
+ concede: standardMoves<TestGameState>({
212
+ include: ["concede"],
213
+ }).concede!,
214
+ };
215
+
216
+ // Lorcana zones (simplified)
217
+ const lorcanaZones: Record<string, CardZoneConfig> = {
218
+ deck: {
219
+ id: "deck" as ZoneId,
220
+ name: "zones.deck",
221
+ visibility: "secret",
222
+ ordered: true,
223
+ owner: undefined,
224
+ faceDown: true,
225
+ maxSize: 60,
226
+ },
227
+ hand: {
228
+ id: "hand" as ZoneId,
229
+ name: "zones.hand",
230
+ visibility: "private",
231
+ ordered: false,
232
+ owner: undefined,
233
+ faceDown: false,
234
+ maxSize: undefined,
235
+ },
236
+ inkwell: {
237
+ id: "inkwell" as ZoneId,
238
+ name: "zones.inkwell",
239
+ visibility: "public",
240
+ ordered: false,
241
+ owner: undefined,
242
+ faceDown: true,
243
+ maxSize: undefined,
244
+ },
245
+ play: {
246
+ id: "play" as ZoneId,
247
+ name: "zones.play",
248
+ visibility: "public",
249
+ ordered: false,
250
+ owner: undefined,
251
+ faceDown: false,
252
+ maxSize: undefined,
253
+ },
254
+ discard: {
255
+ id: "discard" as ZoneId,
256
+ name: "zones.discard",
257
+ visibility: "public",
258
+ ordered: false,
259
+ owner: undefined,
260
+ faceDown: false,
261
+ maxSize: undefined,
262
+ },
263
+ };
264
+
265
+ // Lorcana flow (simplified)
266
+ const lorcanaFlow: FlowDefinition<TestGameState> = {
267
+ turn: {
268
+ initialPhase: "beginning",
269
+ phases: {
270
+ beginning: {
271
+ order: 1,
272
+ next: "main",
273
+ onBegin: (_context) => {},
274
+ endIf: () => true,
275
+ },
276
+ main: {
277
+ order: 2,
278
+ next: "end",
279
+ onBegin: (_context) => {},
280
+ },
281
+ end: {
282
+ order: 3,
283
+ next: "beginning",
284
+ onBegin: (_context) => {},
285
+ endIf: () => true,
286
+ },
287
+ },
288
+ },
289
+ };
290
+
291
+ export function createMockLorcanaGame(): GameDefinition<
292
+ TestGameState,
293
+ TestMoves
294
+ > {
295
+ return {
296
+ name: "Test Lorcana Game",
297
+ zones: lorcanaZones,
298
+ flow: lorcanaFlow,
299
+ moves: lorcanaMoves,
300
+
301
+ // Configure engine's tracker system
302
+ trackers: {
303
+ perTurn: ["hasInked"],
304
+ perPlayer: true,
305
+ },
306
+
307
+ /**
308
+ * Setup function - MASSIVELY SIMPLIFIED!
309
+ *
310
+ * BEFORE: 70+ lines tracking activePlayerId, turnNumber, gamePhase, firstPlayerDetermined, player zones
311
+ * AFTER: 15 lines - just initialize game-specific data!
312
+ */
313
+ setup: (players) => {
314
+ const playerIds = players.map((p) => p.id);
315
+ const loreScores: Record<string, number> = {};
316
+
317
+ for (const playerId of playerIds) {
318
+ loreScores[playerId] = 0;
319
+ }
320
+
321
+ return {
322
+ effects: [],
323
+ bag: [],
324
+ loreScores,
325
+ };
326
+ },
327
+ };
328
+ }