@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,555 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+ import type { Patch } from "immer";
3
+ import { MultiplayerEngine } from "../engine/multiplayer-engine";
4
+ import type { GameDefinition } from "../game-definition/game-definition";
5
+ import type { GameMoveDefinitions } from "../game-definition/move-definitions";
6
+ import { createPlayerId } from "../types";
7
+
8
+ /**
9
+ * MultiplayerEngine Tests
10
+ *
11
+ * Tests the multiplayer engine wrapper that encapsulates
12
+ * server-authoritative patterns for network gameplay.
13
+ */
14
+
15
+ type TestGameState = {
16
+ players: Array<{
17
+ id: string;
18
+ name: string;
19
+ hand: string[];
20
+ score: number;
21
+ }>;
22
+ currentPlayerIndex: number;
23
+ deck: string[];
24
+ turnNumber: number;
25
+ };
26
+
27
+ type TestMoves = {
28
+ drawCard: Record<string, never>;
29
+ playCard: { cardId: string };
30
+ endTurn: Record<string, never>;
31
+ };
32
+
33
+ describe("MultiplayerEngine", () => {
34
+ const createTestGame = (): GameDefinition<TestGameState, TestMoves> => {
35
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
36
+ drawCard: {
37
+ reducer: (draft) => {
38
+ const card = draft.deck.pop();
39
+ if (card) {
40
+ const player = draft.players[draft.currentPlayerIndex];
41
+ if (player) {
42
+ player.hand.push(card);
43
+ }
44
+ }
45
+ },
46
+ },
47
+ playCard: {
48
+ reducer: (draft, context) => {
49
+ const player = draft.players[draft.currentPlayerIndex];
50
+ if (player && context.params?.cardId) {
51
+ const cardId = context.params.cardId as string;
52
+ const cardIndex = player.hand.indexOf(cardId);
53
+ if (cardIndex >= 0) {
54
+ player.hand.splice(cardIndex, 1);
55
+ player.score += 1;
56
+ }
57
+ }
58
+ },
59
+ },
60
+ endTurn: {
61
+ reducer: (draft) => {
62
+ draft.currentPlayerIndex =
63
+ (draft.currentPlayerIndex + 1) % draft.players.length;
64
+ draft.turnNumber += 1;
65
+ },
66
+ },
67
+ };
68
+
69
+ return {
70
+ name: "Test Game",
71
+ setup: (players) => ({
72
+ players: players.map((p) => ({
73
+ id: p.id,
74
+ name: p.name || "Player",
75
+ hand: [],
76
+ score: 0,
77
+ })),
78
+ currentPlayerIndex: 0,
79
+ deck: ["card1", "card2", "card3", "card4", "card5"],
80
+ turnNumber: 1,
81
+ }),
82
+ moves,
83
+ };
84
+ };
85
+
86
+ describe("Server Mode", () => {
87
+ it("should create server-mode engine", () => {
88
+ const gameDefinition = createTestGame();
89
+ const players = [
90
+ { id: createPlayerId("p1"), name: "Alice" },
91
+ { id: createPlayerId("p2"), name: "Bob" },
92
+ ];
93
+
94
+ const server = new MultiplayerEngine(gameDefinition, players, {
95
+ mode: "server",
96
+ seed: "test-seed",
97
+ });
98
+
99
+ expect(server.getMode()).toBe("server");
100
+ expect(server.getState()).toBeDefined();
101
+ });
102
+
103
+ it("should execute moves and broadcast patches via callback", () => {
104
+ const gameDefinition = createTestGame();
105
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
106
+
107
+ const onPatchBroadcast = mock((broadcast) => {
108
+ expect(broadcast.patches).toBeDefined();
109
+ expect(broadcast.inversePatches).toBeDefined();
110
+ expect(broadcast.moveId).toBe("drawCard");
111
+ expect(broadcast.historyIndex).toBeGreaterThanOrEqual(0);
112
+ });
113
+
114
+ const server = new MultiplayerEngine(gameDefinition, players, {
115
+ mode: "server",
116
+ onPatchBroadcast,
117
+ });
118
+
119
+ const result = server.executeMove("drawCard", {
120
+ playerId: createPlayerId("p1"),
121
+ params: {},
122
+ });
123
+
124
+ expect(result.success).toBe(true);
125
+ expect(onPatchBroadcast).toHaveBeenCalledTimes(1);
126
+
127
+ // Verify state changed
128
+ const state = server.getState();
129
+ expect(state.players[0]?.hand.length).toBe(1);
130
+ });
131
+
132
+ it("should call onMoveRejected callback for invalid moves", () => {
133
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
134
+ drawCard: {
135
+ condition: () => false, // Always fails
136
+ reducer: () => {},
137
+ },
138
+ playCard: { reducer: () => {} },
139
+ endTurn: { reducer: () => {} },
140
+ };
141
+
142
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
143
+ name: "Test",
144
+ setup: createTestGame().setup,
145
+ moves,
146
+ };
147
+
148
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
149
+
150
+ const onMoveRejected = mock((moveId, error, errorCode) => {
151
+ expect(moveId).toBe("drawCard");
152
+ expect(error).toContain("condition not met");
153
+ expect(errorCode).toBe("CONDITION_FAILED");
154
+ });
155
+
156
+ const server = new MultiplayerEngine(gameDefinition, players, {
157
+ mode: "server",
158
+ onMoveRejected,
159
+ });
160
+
161
+ const result = server.executeMove("drawCard", {
162
+ playerId: createPlayerId("p1"),
163
+ params: {},
164
+ });
165
+
166
+ expect(result.success).toBe(false);
167
+ expect(onMoveRejected).toHaveBeenCalledTimes(1);
168
+ });
169
+
170
+ it("should provide catchup patches for reconnecting clients", () => {
171
+ const gameDefinition = createTestGame();
172
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
173
+
174
+ const server = new MultiplayerEngine(gameDefinition, players, {
175
+ mode: "server",
176
+ });
177
+
178
+ // Execute 3 moves
179
+ server.executeMove("drawCard", {
180
+ playerId: createPlayerId("p1"),
181
+ params: {},
182
+ });
183
+ server.executeMove("playCard", {
184
+ playerId: createPlayerId("p1"),
185
+ params: { cardId: "card5" },
186
+ });
187
+ server.executeMove("endTurn", {
188
+ playerId: createPlayerId("p1"),
189
+ params: {},
190
+ });
191
+
192
+ // Get all patches
193
+ const allPatches = server.getCatchupPatches(0);
194
+ expect(allPatches.length).toBeGreaterThan(0);
195
+
196
+ // Get patches from index 1
197
+ const partialPatches = server.getCatchupPatches(1);
198
+ expect(partialPatches.length).toBeLessThan(allPatches.length);
199
+ });
200
+
201
+ it("should manage client registration and tracking", () => {
202
+ const gameDefinition = createTestGame();
203
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
204
+
205
+ const server = new MultiplayerEngine(gameDefinition, players, {
206
+ mode: "server",
207
+ });
208
+
209
+ // Register clients
210
+ server.registerClient("client-1", -1);
211
+ server.registerClient("client-2", -1);
212
+
213
+ const clients = server.getAllClients();
214
+ expect(clients.length).toBe(2);
215
+ expect(clients[0]?.connected).toBe(true);
216
+
217
+ // Update sync index
218
+ server.updateClientSyncIndex("client-1", 5);
219
+ const client1 = server.getClientState("client-1");
220
+ expect(client1?.lastSyncedIndex).toBe(5);
221
+
222
+ // Unregister client
223
+ server.unregisterClient("client-1");
224
+ const disconnectedClient = server.getClientState("client-1");
225
+ expect(disconnectedClient?.connected).toBe(false);
226
+ });
227
+
228
+ it("should provide current history index", () => {
229
+ const gameDefinition = createTestGame();
230
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
231
+
232
+ const server = new MultiplayerEngine(gameDefinition, players, {
233
+ mode: "server",
234
+ });
235
+
236
+ expect(server.getCurrentHistoryIndex()).toBe(-1); // No moves yet
237
+
238
+ server.executeMove("drawCard", {
239
+ playerId: createPlayerId("p1"),
240
+ params: {},
241
+ });
242
+ expect(server.getCurrentHistoryIndex()).toBe(0);
243
+
244
+ server.executeMove("endTurn", {
245
+ playerId: createPlayerId("p1"),
246
+ params: {},
247
+ });
248
+ expect(server.getCurrentHistoryIndex()).toBe(1);
249
+ });
250
+
251
+ it("should throw error when client tries server-only operations", () => {
252
+ const gameDefinition = createTestGame();
253
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
254
+
255
+ const client = new MultiplayerEngine(gameDefinition, players, {
256
+ mode: "client",
257
+ });
258
+
259
+ expect(() =>
260
+ client.executeMove("drawCard", {
261
+ playerId: createPlayerId("p1"),
262
+ params: {},
263
+ }),
264
+ ).toThrow("Only server can execute moves");
265
+
266
+ expect(() => client.getCatchupPatches()).toThrow(
267
+ "Only server can provide catchup patches",
268
+ );
269
+
270
+ expect(() => client.registerClient("test")).toThrow(
271
+ "Only server can register clients",
272
+ );
273
+
274
+ expect(() => client.getHistory()).toThrow(
275
+ "Only server maintains authoritative history",
276
+ );
277
+ });
278
+ });
279
+
280
+ describe("Client Mode", () => {
281
+ it("should create client-mode engine", () => {
282
+ const gameDefinition = createTestGame();
283
+ const players = [
284
+ { id: createPlayerId("p1"), name: "Alice" },
285
+ { id: createPlayerId("p2"), name: "Bob" },
286
+ ];
287
+
288
+ const client = new MultiplayerEngine(gameDefinition, players, {
289
+ mode: "client",
290
+ });
291
+
292
+ expect(client.getMode()).toBe("client");
293
+ expect(client.getState()).toBeDefined();
294
+ });
295
+
296
+ it("should apply patches from server", () => {
297
+ const gameDefinition = createTestGame();
298
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
299
+
300
+ const onPatchesApplied = mock((patches: Patch[]) => {
301
+ expect(patches.length).toBeGreaterThan(0);
302
+ });
303
+
304
+ const client = new MultiplayerEngine(gameDefinition, players, {
305
+ mode: "client",
306
+ onPatchesApplied,
307
+ });
308
+
309
+ // Simulate receiving patches from server
310
+ // Create patches by executing on a server engine
311
+ const server = new MultiplayerEngine(gameDefinition, players, {
312
+ mode: "server",
313
+ });
314
+
315
+ const result = server.executeMove("drawCard", {
316
+ playerId: createPlayerId("p1"),
317
+ params: {},
318
+ });
319
+
320
+ if (result.success) {
321
+ // Client applies patches
322
+ client.applyServerPatches(result.patches);
323
+
324
+ expect(onPatchesApplied).toHaveBeenCalledTimes(1);
325
+
326
+ // States should match
327
+ expect(client.getState()).toEqual(server.getState());
328
+ }
329
+ });
330
+
331
+ it("should throw error when server tries client-only operations", () => {
332
+ const gameDefinition = createTestGame();
333
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
334
+
335
+ const server = new MultiplayerEngine(gameDefinition, players, {
336
+ mode: "server",
337
+ });
338
+
339
+ expect(() => server.applyServerPatches([])).toThrow(
340
+ "Only clients can apply server patches",
341
+ );
342
+ });
343
+ });
344
+
345
+ describe("Server-Client Synchronization", () => {
346
+ it("should keep server and clients in sync through patches", () => {
347
+ const gameDefinition = createTestGame();
348
+ const players = [
349
+ { id: createPlayerId("p1"), name: "Alice" },
350
+ { id: createPlayerId("p2"), name: "Bob" },
351
+ ];
352
+
353
+ // Create server
354
+ const server = new MultiplayerEngine(gameDefinition, players, {
355
+ mode: "server",
356
+ seed: "test-seed",
357
+ });
358
+
359
+ // Create clients
360
+ const client1 = new MultiplayerEngine(gameDefinition, players, {
361
+ mode: "client",
362
+ });
363
+
364
+ const client2 = new MultiplayerEngine(gameDefinition, players, {
365
+ mode: "client",
366
+ });
367
+
368
+ // Initial states match
369
+ expect(server.getState()).toEqual(client1.getState());
370
+ expect(server.getState()).toEqual(client2.getState());
371
+
372
+ // Execute move on server
373
+ const result = server.executeMove("drawCard", {
374
+ playerId: createPlayerId("p1"),
375
+ params: {},
376
+ });
377
+
378
+ if (result.success) {
379
+ // Broadcast to clients
380
+ client1.applyServerPatches(result.patches);
381
+ client2.applyServerPatches(result.patches);
382
+
383
+ // All states match
384
+ expect(server.getState()).toEqual(client1.getState());
385
+ expect(server.getState()).toEqual(client2.getState());
386
+
387
+ // Verify move executed
388
+ expect(server.getState().players[0]?.hand.length).toBe(1);
389
+ }
390
+ });
391
+
392
+ it("should handle multiple sequential moves with synchronization", () => {
393
+ const gameDefinition = createTestGame();
394
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
395
+
396
+ const server = new MultiplayerEngine(gameDefinition, players, {
397
+ mode: "server",
398
+ });
399
+
400
+ const client = new MultiplayerEngine(gameDefinition, players, {
401
+ mode: "client",
402
+ });
403
+
404
+ const moves = [
405
+ { move: "drawCard", params: {} },
406
+ { move: "playCard", params: { cardId: "card5" } },
407
+ { move: "endTurn", params: {} },
408
+ ];
409
+
410
+ for (const moveData of moves) {
411
+ const result = server.executeMove(moveData.move, {
412
+ playerId: createPlayerId("p1"),
413
+ params: moveData.params,
414
+ });
415
+
416
+ if (result.success) {
417
+ client.applyServerPatches(result.patches);
418
+ expect(server.getState()).toEqual(client.getState());
419
+ }
420
+ }
421
+
422
+ // Final state verification
423
+ const finalState = server.getState();
424
+ expect(finalState.players[0]?.hand.length).toBe(0);
425
+ expect(finalState.players[0]?.score).toBe(1);
426
+ expect(finalState.turnNumber).toBe(2);
427
+ });
428
+
429
+ it("should handle client reconnection with batch patches", () => {
430
+ const gameDefinition = createTestGame();
431
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
432
+
433
+ const server = new MultiplayerEngine(gameDefinition, players, {
434
+ mode: "server",
435
+ });
436
+
437
+ // Execute moves while client is disconnected
438
+ server.executeMove("drawCard", {
439
+ playerId: createPlayerId("p1"),
440
+ params: {},
441
+ });
442
+ server.executeMove("endTurn", {
443
+ playerId: createPlayerId("p1"),
444
+ params: {},
445
+ });
446
+
447
+ // Client reconnects
448
+ const reconnectedClient = new MultiplayerEngine(gameDefinition, players, {
449
+ mode: "client",
450
+ });
451
+
452
+ // Get all patches and apply
453
+ const allPatches = server.getCatchupPatches();
454
+ reconnectedClient.applyServerPatches(allPatches);
455
+
456
+ // Client is synced
457
+ expect(reconnectedClient.getState()).toEqual(server.getState());
458
+ });
459
+ });
460
+
461
+ describe("Common Operations", () => {
462
+ it("should support getState on both server and client", () => {
463
+ const gameDefinition = createTestGame();
464
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
465
+
466
+ const server = new MultiplayerEngine(gameDefinition, players, {
467
+ mode: "server",
468
+ });
469
+
470
+ const client = new MultiplayerEngine(gameDefinition, players, {
471
+ mode: "client",
472
+ });
473
+
474
+ expect(server.getState()).toBeDefined();
475
+ expect(client.getState()).toBeDefined();
476
+ });
477
+
478
+ it("should support getPlayerView on both server and client", () => {
479
+ const gameDefinition = createTestGame();
480
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
481
+
482
+ const server = new MultiplayerEngine(gameDefinition, players, {
483
+ mode: "server",
484
+ });
485
+
486
+ const client = new MultiplayerEngine(gameDefinition, players, {
487
+ mode: "client",
488
+ });
489
+
490
+ const serverView = server.getPlayerView(createPlayerId("p1"));
491
+ const clientView = client.getPlayerView(createPlayerId("p1"));
492
+
493
+ expect(serverView).toBeDefined();
494
+ expect(clientView).toBeDefined();
495
+ });
496
+
497
+ it("should support canExecuteMove on both server and client", () => {
498
+ const gameDefinition = createTestGame();
499
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
500
+
501
+ const server = new MultiplayerEngine(gameDefinition, players, {
502
+ mode: "server",
503
+ });
504
+
505
+ const client = new MultiplayerEngine(gameDefinition, players, {
506
+ mode: "client",
507
+ });
508
+
509
+ const canExecuteServer = server.canExecuteMove("drawCard", {
510
+ playerId: createPlayerId("p1"),
511
+ params: {},
512
+ });
513
+
514
+ const canExecuteClient = client.canExecuteMove("drawCard", {
515
+ playerId: createPlayerId("p1"),
516
+ params: {},
517
+ });
518
+
519
+ expect(canExecuteServer).toBe(true);
520
+ expect(canExecuteClient).toBe(true);
521
+ });
522
+
523
+ it("should support getValidMoves on both server and client", () => {
524
+ const gameDefinition = createTestGame();
525
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
526
+
527
+ const server = new MultiplayerEngine(gameDefinition, players, {
528
+ mode: "server",
529
+ });
530
+
531
+ const client = new MultiplayerEngine(gameDefinition, players, {
532
+ mode: "client",
533
+ });
534
+
535
+ const serverMoves = server.getValidMoves(createPlayerId("p1"));
536
+ const clientMoves = client.getValidMoves(createPlayerId("p1"));
537
+
538
+ expect(serverMoves.length).toBeGreaterThan(0);
539
+ expect(clientMoves.length).toBeGreaterThan(0);
540
+ });
541
+
542
+ it("should provide access to underlying engine", () => {
543
+ const gameDefinition = createTestGame();
544
+ const players = [{ id: createPlayerId("p1"), name: "Alice" }];
545
+
546
+ const server = new MultiplayerEngine(gameDefinition, players, {
547
+ mode: "server",
548
+ });
549
+
550
+ const engine = server.getEngine();
551
+ expect(engine).toBeDefined();
552
+ expect(engine.getState).toBeDefined();
553
+ });
554
+ });
555
+ });
@@ -0,0 +1,114 @@
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 { createMockOnePieceGame } from "./createMockOnePieceGame";
5
+
6
+ /**
7
+ * One Piece Card Game - Engine Feature Tests
8
+ *
9
+ * Refactored to showcase:
10
+ * ✅ High-level zone utilities (createDeck, drawCards, mulligan, bulkMove)
11
+ * ✅ Flow context access (isFirstTurn, turn)
12
+ * ✅ Standard moves (pass, concede)
13
+ * ✅ Massive simplification (10 fields → 2 fields, -80%)
14
+ */
15
+ describe("One Piece Game - Refactored Engine Features", () => {
16
+ it("should initialize game with ONLY game-specific state", () => {
17
+ const gameDefinition = createMockOnePieceGame();
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.battleAllowed).toBe(false);
25
+ expect(state.leaderLife).toBeDefined();
26
+
27
+ // ✅ REMOVED: Massive reduction
28
+ // @ts-expect-error
29
+ expect(state.phase).toBeUndefined();
30
+ // @ts-expect-error
31
+ expect(state.setupStep).toBeUndefined();
32
+ // @ts-expect-error
33
+ expect(state.turn).toBeUndefined();
34
+ // @ts-expect-error
35
+ expect(state.currentPlayer).toBeUndefined();
36
+ // @ts-expect-error
37
+ expect(state.firstTurn).toBeUndefined();
38
+ // @ts-expect-error
39
+ expect(state.mulliganOffered).toBeUndefined();
40
+ // @ts-expect-error
41
+ expect(state.donThisTurn).toBeUndefined();
42
+ });
43
+
44
+ it("should have proper zone configuration", () => {
45
+ const gameDefinition = createMockOnePieceGame();
46
+ const zones = gameDefinition.zones;
47
+
48
+ // Verify One Piece zones
49
+ expect(zones?.deck).toBeDefined();
50
+ expect(zones?.hand).toBeDefined();
51
+ expect(zones?.donDeck).toBeDefined();
52
+ expect(zones?.donArea).toBeDefined();
53
+ expect(zones?.leader).toBeDefined();
54
+ expect(zones?.characters).toBeDefined();
55
+ expect(zones?.stage).toBeDefined();
56
+ expect(zones?.life).toBeDefined();
57
+ expect(zones?.discard).toBeDefined();
58
+
59
+ expect(zones?.deck?.maxSize).toBe(50);
60
+ expect(zones?.donDeck?.maxSize).toBe(10);
61
+ expect(zones?.leader?.maxSize).toBe(1);
62
+ expect(zones?.life?.maxSize).toBe(5);
63
+ });
64
+
65
+ it("should use ALL high-level zone utilities", () => {
66
+ // ✅ One Piece uses ALL 4 utilities!
67
+ // - createDeck() for deck initialization
68
+ // - drawCards() for drawing
69
+ // - mulligan() for redraw
70
+ // - bulkMove() for life card placement
71
+
72
+ const gameDefinition = createMockOnePieceGame();
73
+ expect(gameDefinition.moves.initializeDecks).toBeDefined();
74
+ expect(gameDefinition.moves.drawOpeningHand).toBeDefined();
75
+ expect(gameDefinition.moves.decideMulligan).toBeDefined();
76
+ expect(gameDefinition.moves.placeLifeCards).toBeDefined();
77
+ });
78
+
79
+ it("should use flow context for first turn draw skip", () => {
80
+ const gameDefinition = createMockOnePieceGame();
81
+
82
+ // draw move uses context.flow.isFirstTurn and context.flow.currentPlayer
83
+ const draw = gameDefinition.moves.draw;
84
+ expect(draw.condition).toBeDefined();
85
+
86
+ // First player skips draw on first turn
87
+ });
88
+
89
+ it("should use flow context for DON!! placement", () => {
90
+ const gameDefinition = createMockOnePieceGame();
91
+
92
+ // placeDon uses context.flow.turn to determine DON!! count
93
+ const placeDon = gameDefinition.moves.placeDon;
94
+ expect(placeDon.reducer).toBeDefined();
95
+ });
96
+
97
+ it("should use standard moves", () => {
98
+ const gameDefinition = createMockOnePieceGame();
99
+
100
+ expect(gameDefinition.moves.pass).toBeDefined();
101
+ expect(gameDefinition.moves.concede).toBeDefined();
102
+ });
103
+
104
+ it("should demonstrate largest boilerplate reduction", () => {
105
+ // State fields: 10 → 2 (-80%)
106
+ // 593 lines → 430 lines (-27% - HIGHEST reduction!)
107
+
108
+ const gameDefinition = createMockOnePieceGame();
109
+ const players = createTestPlayers(2);
110
+ const state = gameDefinition.setup(players);
111
+
112
+ expect(Object.keys(state).length).toBe(2);
113
+ });
114
+ });