@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,469 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { RuleEngine } from "../engine/rule-engine";
3
+ import type { GameDefinition } from "../game-definition/game-definition";
4
+ import type { GameMoveDefinitions } from "../game-definition/move-definitions";
5
+ import { createPlayerId } from "../types";
6
+
7
+ /**
8
+ * Task 16.1, 16.2: Integration Tests - Server-Authoritative Pattern
9
+ *
10
+ * Tests the complete network synchronization pattern:
11
+ * - Server receives move from client
12
+ * - Server validates and executes move
13
+ * - Server broadcasts patches to all clients
14
+ * - Clients apply patches to sync state
15
+ *
16
+ * This validates that the patch-based synchronization enables
17
+ * authoritative multiplayer gameplay.
18
+ */
19
+
20
+ type MultiplayerGameState = {
21
+ players: Array<{
22
+ id: string;
23
+ name: string;
24
+ hand: string[];
25
+ score: number;
26
+ }>;
27
+ currentPlayerIndex: number;
28
+ deck: string[];
29
+ turnNumber: number;
30
+ phase: "draw" | "play" | "ended";
31
+ };
32
+
33
+ type MultiplayerMoves = {
34
+ drawCard: Record<string, never>;
35
+ playCard: { cardId: string };
36
+ endTurn: Record<string, never>;
37
+ };
38
+
39
+ describe("Integration - Network Synchronization", () => {
40
+ describe("Task 16.1: Server-Authoritative Pattern", () => {
41
+ it("should execute move on server and broadcast patches to clients", () => {
42
+ const moves: GameMoveDefinitions<MultiplayerGameState, MultiplayerMoves> =
43
+ {
44
+ drawCard: {
45
+ reducer: (draft) => {
46
+ const card = draft.deck.pop();
47
+ if (card) {
48
+ const player = draft.players[draft.currentPlayerIndex];
49
+ if (player) {
50
+ player.hand.push(card);
51
+ }
52
+ }
53
+ },
54
+ },
55
+ playCard: {
56
+ reducer: (draft, context) => {
57
+ const player = draft.players[draft.currentPlayerIndex];
58
+ if (player && context.params?.cardId) {
59
+ const cardId = context.params.cardId as string;
60
+ const cardIndex = player.hand.indexOf(cardId);
61
+ if (cardIndex >= 0) {
62
+ player.hand.splice(cardIndex, 1);
63
+ player.score += 1;
64
+ }
65
+ }
66
+ },
67
+ },
68
+ endTurn: {
69
+ reducer: (draft) => {
70
+ draft.currentPlayerIndex =
71
+ (draft.currentPlayerIndex + 1) % draft.players.length;
72
+ draft.turnNumber += 1;
73
+ },
74
+ },
75
+ };
76
+
77
+ const gameDefinition: GameDefinition<
78
+ MultiplayerGameState,
79
+ MultiplayerMoves
80
+ > = {
81
+ name: "Multiplayer Test Game",
82
+ setup: (players) => ({
83
+ players: players.map((p) => ({
84
+ id: p.id,
85
+ name: p.name || "Player",
86
+ hand: [],
87
+ score: 0,
88
+ })),
89
+ currentPlayerIndex: 0,
90
+ deck: ["card1", "card2", "card3", "card4", "card5"],
91
+ turnNumber: 1,
92
+ phase: "draw",
93
+ }),
94
+ moves,
95
+ };
96
+
97
+ const players = [
98
+ { id: createPlayerId("p1"), name: "Alice" },
99
+ { id: createPlayerId("p2"), name: "Bob" },
100
+ ];
101
+
102
+ // Server creates authoritative engine
103
+ const server = new RuleEngine(gameDefinition, players, {
104
+ seed: "server-123",
105
+ });
106
+
107
+ // Clients create local engines (synchronized via patches)
108
+ const client1 = new RuleEngine(gameDefinition, players, {
109
+ seed: "client-1",
110
+ });
111
+ const client2 = new RuleEngine(gameDefinition, players, {
112
+ seed: "client-2",
113
+ });
114
+
115
+ // Initial state should match
116
+ expect(server.getState()).toEqual(client1.getState());
117
+ expect(server.getState()).toEqual(client2.getState());
118
+
119
+ // Client 1 sends move to server
120
+ const moveContext = {
121
+ playerId: createPlayerId("p1"),
122
+ params: {},
123
+ };
124
+
125
+ // Server executes move
126
+ const result = server.executeMove("drawCard", moveContext);
127
+
128
+ expect(result.success).toBe(true);
129
+ if (result.success) {
130
+ // Server broadcasts patches to all clients
131
+ const patches = result.patches;
132
+
133
+ // Clients apply patches to synchronize
134
+ client1.applyPatches(patches);
135
+ client2.applyPatches(patches);
136
+
137
+ // All states should now match
138
+ const serverState = server.getState();
139
+ const client1State = client1.getState();
140
+ const client2State = client2.getState();
141
+
142
+ expect(serverState).toEqual(client1State);
143
+ expect(serverState).toEqual(client2State);
144
+
145
+ // Verify move actually executed
146
+ expect(serverState.players[0]?.hand.length).toBe(1);
147
+ expect(serverState.deck.length).toBe(4);
148
+ }
149
+ });
150
+
151
+ it("should reject invalid moves on server before broadcasting", () => {
152
+ const moves: GameMoveDefinitions<MultiplayerGameState, MultiplayerMoves> =
153
+ {
154
+ drawCard: {
155
+ condition: (state) => state.phase === "draw",
156
+ reducer: (draft) => {
157
+ const card = draft.deck.pop();
158
+ if (card) {
159
+ const player = draft.players[draft.currentPlayerIndex];
160
+ if (player) {
161
+ player.hand.push(card);
162
+ }
163
+ }
164
+ },
165
+ },
166
+ playCard: { reducer: () => {} },
167
+ endTurn: { reducer: () => {} },
168
+ };
169
+
170
+ const gameDefinition: GameDefinition<
171
+ MultiplayerGameState,
172
+ MultiplayerMoves
173
+ > = {
174
+ name: "Validation Test",
175
+ setup: (players) => ({
176
+ players: players.map((p) => ({
177
+ id: p.id,
178
+ name: p.name || "Player",
179
+ hand: [],
180
+ score: 0,
181
+ })),
182
+ currentPlayerIndex: 0,
183
+ deck: ["card1"],
184
+ turnNumber: 1,
185
+ phase: "play", // Not "draw" phase
186
+ }),
187
+ moves,
188
+ };
189
+
190
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
191
+
192
+ const server = new RuleEngine(gameDefinition, players);
193
+ const client = new RuleEngine(gameDefinition, players);
194
+
195
+ // Client attempts invalid move
196
+ const result = server.executeMove("drawCard", {
197
+ playerId: createPlayerId("p1"),
198
+ params: {},
199
+ });
200
+
201
+ // Server rejects move
202
+ expect(result.success).toBe(false);
203
+
204
+ // No patches to broadcast - invalid moves don't have patches
205
+ // Type guard: when success is false, patches property doesn't exist
206
+ if (result.success === false) {
207
+ // patches property doesn't exist on error result
208
+ expect("patches" in result).toBe(false);
209
+ }
210
+
211
+ // Client state unchanged
212
+ expect(server.getState()).toEqual(client.getState());
213
+ });
214
+ });
215
+
216
+ describe("Task 16.2: Network Synchronization Pattern", () => {
217
+ it("should handle multiple moves with incremental patch synchronization", () => {
218
+ const moves: GameMoveDefinitions<MultiplayerGameState, MultiplayerMoves> =
219
+ {
220
+ drawCard: {
221
+ reducer: (draft) => {
222
+ const card = draft.deck.pop();
223
+ if (card) {
224
+ const player = draft.players[draft.currentPlayerIndex];
225
+ if (player) {
226
+ player.hand.push(card);
227
+ }
228
+ }
229
+ },
230
+ },
231
+ playCard: {
232
+ reducer: (draft, context) => {
233
+ const player = draft.players[draft.currentPlayerIndex];
234
+ if (player && context.params?.cardId) {
235
+ const cardId = context.params.cardId as string;
236
+ const cardIndex = player.hand.indexOf(cardId);
237
+ if (cardIndex >= 0) {
238
+ player.hand.splice(cardIndex, 1);
239
+ player.score += 1;
240
+ }
241
+ }
242
+ },
243
+ },
244
+ endTurn: {
245
+ reducer: (draft) => {
246
+ draft.currentPlayerIndex =
247
+ (draft.currentPlayerIndex + 1) % draft.players.length;
248
+ draft.turnNumber += 1;
249
+ },
250
+ },
251
+ };
252
+
253
+ const gameDefinition: GameDefinition<
254
+ MultiplayerGameState,
255
+ MultiplayerMoves
256
+ > = {
257
+ name: "Incremental Sync Test",
258
+ setup: (players) => ({
259
+ players: players.map((p) => ({
260
+ id: p.id,
261
+ name: p.name || "Player",
262
+ hand: [],
263
+ score: 0,
264
+ })),
265
+ currentPlayerIndex: 0,
266
+ deck: ["card1", "card2", "card3"],
267
+ turnNumber: 1,
268
+ phase: "draw",
269
+ }),
270
+ moves,
271
+ };
272
+
273
+ const players = [
274
+ { id: createPlayerId("p1"), name: "Alice" },
275
+ { id: createPlayerId("p2"), name: "Bob" },
276
+ ];
277
+
278
+ const server = new RuleEngine(gameDefinition, players);
279
+ const client = new RuleEngine(gameDefinition, players);
280
+
281
+ // Simulate 3 moves with incremental synchronization
282
+ const moves_to_execute = [
283
+ { move: "drawCard", params: {} },
284
+ { move: "playCard", params: { cardId: "card3" } }, // card3 was drawn
285
+ { move: "endTurn", params: {} },
286
+ ];
287
+
288
+ for (const moveToExecute of moves_to_execute) {
289
+ const result = server.executeMove(moveToExecute.move, {
290
+ playerId: createPlayerId("p1"),
291
+ params: moveToExecute.params,
292
+ });
293
+
294
+ if (result.success) {
295
+ // Client applies patches incrementally
296
+ client.applyPatches(result.patches);
297
+
298
+ // States should match after each move
299
+ expect(server.getState()).toEqual(client.getState());
300
+ }
301
+ }
302
+
303
+ // Final verification
304
+ const finalState = server.getState();
305
+ expect(finalState.players[0]?.hand.length).toBe(0); // Played card
306
+ expect(finalState.players[0]?.score).toBe(1); // Scored 1 point
307
+ expect(finalState.currentPlayerIndex).toBe(1); // Next player
308
+ expect(finalState.turnNumber).toBe(2); // Turn incremented
309
+ });
310
+
311
+ it("should support batch patch application for reconnecting clients", () => {
312
+ const moves: GameMoveDefinitions<MultiplayerGameState, MultiplayerMoves> =
313
+ {
314
+ drawCard: {
315
+ reducer: (draft) => {
316
+ const card = draft.deck.pop();
317
+ if (card) {
318
+ const player = draft.players[draft.currentPlayerIndex];
319
+ if (player) {
320
+ player.hand.push(card);
321
+ }
322
+ }
323
+ },
324
+ },
325
+ playCard: { reducer: () => {} },
326
+ endTurn: {
327
+ reducer: (draft) => {
328
+ draft.currentPlayerIndex =
329
+ (draft.currentPlayerIndex + 1) % draft.players.length;
330
+ draft.turnNumber += 1;
331
+ },
332
+ },
333
+ };
334
+
335
+ const gameDefinition: GameDefinition<
336
+ MultiplayerGameState,
337
+ MultiplayerMoves
338
+ > = {
339
+ name: "Batch Sync Test",
340
+ setup: (players) => ({
341
+ players: players.map((p) => ({
342
+ id: p.id,
343
+ name: p.name || "Player",
344
+ hand: [],
345
+ score: 0,
346
+ })),
347
+ currentPlayerIndex: 0,
348
+ deck: ["card1", "card2", "card3", "card4"],
349
+ turnNumber: 1,
350
+ phase: "draw",
351
+ }),
352
+ moves,
353
+ };
354
+
355
+ const players = [
356
+ { id: createPlayerId("p1"), name: "Alice" },
357
+ { id: createPlayerId("p2"), name: "Bob" },
358
+ ];
359
+
360
+ const server = new RuleEngine(gameDefinition, players);
361
+
362
+ // Execute 3 moves on server while client is disconnected
363
+ server.executeMove("drawCard", {
364
+ playerId: createPlayerId("p1"),
365
+ params: {},
366
+ });
367
+ server.executeMove("endTurn", {
368
+ playerId: createPlayerId("p1"),
369
+ params: {},
370
+ });
371
+ server.executeMove("drawCard", {
372
+ playerId: createPlayerId("p2"),
373
+ params: {},
374
+ });
375
+
376
+ // Client reconnects and needs to catch up
377
+ const disconnectedClient = new RuleEngine(gameDefinition, players);
378
+
379
+ // Server sends all accumulated patches
380
+ const allPatches = server.getPatches();
381
+
382
+ // Client applies batch
383
+ disconnectedClient.applyPatches(allPatches);
384
+
385
+ // Client is now synchronized
386
+ expect(server.getState()).toEqual(disconnectedClient.getState());
387
+
388
+ // Verify state is correct
389
+ const state = disconnectedClient.getState();
390
+ expect(state.players[0]?.hand.length).toBe(1); // Alice drew 1
391
+ expect(state.players[1]?.hand.length).toBe(1); // Bob drew 1
392
+ expect(state.currentPlayerIndex).toBe(1); // Bob's turn
393
+ expect(state.turnNumber).toBe(2);
394
+ });
395
+
396
+ it("should maintain deterministic state across server and clients", () => {
397
+ const moves: GameMoveDefinitions<MultiplayerGameState, MultiplayerMoves> =
398
+ {
399
+ drawCard: {
400
+ reducer: (draft, context) => {
401
+ // Use RNG for deterministic shuffling
402
+ const rng = context.rng;
403
+ if (rng && draft.deck.length > 0) {
404
+ const index = rng.randomInt(0, draft.deck.length - 1);
405
+ const card = draft.deck.splice(index, 1)[0];
406
+ if (card) {
407
+ const player = draft.players[draft.currentPlayerIndex];
408
+ if (player) {
409
+ player.hand.push(card);
410
+ }
411
+ }
412
+ }
413
+ },
414
+ },
415
+ playCard: { reducer: () => {} },
416
+ endTurn: { reducer: () => {} },
417
+ };
418
+
419
+ const gameDefinition: GameDefinition<
420
+ MultiplayerGameState,
421
+ MultiplayerMoves
422
+ > = {
423
+ name: "Deterministic Sync Test",
424
+ setup: (players) => ({
425
+ players: players.map((p) => ({
426
+ id: p.id,
427
+ name: p.name || "Player",
428
+ hand: [],
429
+ score: 0,
430
+ })),
431
+ currentPlayerIndex: 0,
432
+ deck: ["card1", "card2", "card3", "card4", "card5"],
433
+ turnNumber: 1,
434
+ phase: "draw",
435
+ }),
436
+ moves,
437
+ };
438
+
439
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
440
+
441
+ // Server and client use same seed
442
+ const server = new RuleEngine(gameDefinition, players, {
443
+ seed: "deterministic-123",
444
+ });
445
+ const client = new RuleEngine(gameDefinition, players, {
446
+ seed: "deterministic-123",
447
+ });
448
+
449
+ // Execute move on server
450
+ const result = server.executeMove("drawCard", {
451
+ playerId: createPlayerId("p1"),
452
+ params: {},
453
+ });
454
+
455
+ if (result.success) {
456
+ // Client applies patches
457
+ client.applyPatches(result.patches);
458
+
459
+ // States match exactly
460
+ expect(server.getState()).toEqual(client.getState());
461
+
462
+ // Same card drawn due to deterministic RNG
463
+ const serverHand = server.getState().players[0]?.hand;
464
+ const clientHand = client.getState().players[0]?.hand;
465
+ expect(serverHand).toEqual(clientHand);
466
+ }
467
+ });
468
+ });
469
+ });
@@ -0,0 +1,100 @@
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 { createMockLorcanaGame } from "./createMockLorcanaGame";
5
+
6
+ /**
7
+ * Lorcana Card Game - Engine Feature Tests
8
+ *
9
+ * Refactored to showcase:
10
+ * ✅ Engine-managed flow state
11
+ * ✅ High-level zone utilities (drawCards, mulligan)
12
+ * ✅ Tracker system (hasInked, per-card quested tracking)
13
+ * ✅ Standard moves (passTurn, concede)
14
+ * ✅ Simplified state (8 fields → 3 fields)
15
+ */
16
+ describe("Lorcana Game - Refactored Engine Features", () => {
17
+ it("should initialize game with ONLY game-specific state", () => {
18
+ const gameDefinition = createMockLorcanaGame();
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.effects).toBeDefined();
26
+ expect(state.bag).toBeDefined();
27
+ expect(state.loreScores).toBeDefined();
28
+
29
+ // ✅ REMOVED: No manual tracking
30
+ // @ts-expect-error
31
+ expect(state.activePlayerId).toBeUndefined();
32
+ // @ts-expect-error
33
+ expect(state.turnNumber).toBeUndefined();
34
+ // @ts-expect-error
35
+ expect(state.gamePhase).toBeUndefined();
36
+ // @ts-expect-error
37
+ expect(state.firstPlayerDetermined).toBeUndefined();
38
+ });
39
+
40
+ it("should have proper zone configuration", () => {
41
+ const gameDefinition = createMockLorcanaGame();
42
+ const zones = gameDefinition.zones;
43
+
44
+ expect(zones?.deck).toBeDefined();
45
+ expect(zones?.hand).toBeDefined();
46
+ expect(zones?.inkwell).toBeDefined();
47
+ expect(zones?.play).toBeDefined();
48
+ expect(zones?.discard).toBeDefined();
49
+
50
+ expect(zones?.deck?.maxSize).toBe(60);
51
+ expect(zones?.inkwell?.faceDown).toBe(true);
52
+ });
53
+
54
+ it("should use high-level zone utilities", () => {
55
+ // ✅ NEW: zones.mulligan() for alterHand
56
+ // ✅ NEW: zones.drawCards() for drawing
57
+
58
+ const gameDefinition = createMockLorcanaGame();
59
+ expect(gameDefinition.moves.alterHand).toBeDefined();
60
+ expect(gameDefinition.moves.drawCards).toBeDefined();
61
+ });
62
+
63
+ it("should configure tracker system for inking", () => {
64
+ const gameDefinition = createMockLorcanaGame();
65
+
66
+ expect(gameDefinition.trackers).toBeDefined();
67
+ expect(gameDefinition.trackers?.perTurn).toContain("hasInked");
68
+ expect(gameDefinition.trackers?.perPlayer).toBe(true);
69
+ });
70
+
71
+ it("should use tracker system for ink and quest actions", () => {
72
+ const gameDefinition = createMockLorcanaGame();
73
+
74
+ // Inking uses hasInked tracker
75
+ const putInkwell = gameDefinition.moves.putACardIntoTheInkwell;
76
+ expect(putInkwell.condition).toBeDefined();
77
+
78
+ // Questing uses per-card trackers
79
+ const quest = gameDefinition.moves.quest;
80
+ expect(quest.condition).toBeDefined();
81
+ });
82
+
83
+ it("should use standard moves", () => {
84
+ const gameDefinition = createMockLorcanaGame();
85
+
86
+ expect(gameDefinition.moves.passTurn).toBeDefined();
87
+ expect(gameDefinition.moves.concede).toBeDefined();
88
+ });
89
+
90
+ it("should demonstrate boilerplate reduction", () => {
91
+ // State fields: 8 → 3 (-62%)
92
+ // Eliminated player zones from state (engine manages)
93
+
94
+ const gameDefinition = createMockLorcanaGame();
95
+ const players = createTestPlayers(2);
96
+ const state = gameDefinition.setup(players);
97
+
98
+ expect(Object.keys(state).length).toBe(3);
99
+ });
100
+ });