@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,641 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { RuleEngine } from "../../engine/rule-engine";
3
+ import type { FlowDefinition } from "../../flow/flow-definition";
4
+ import type { GameDefinition } from "../../game-definition/game-definition";
5
+ import type { GameMoveDefinitions } from "../../game-definition/move-definitions";
6
+ import { createPlayerId, type PlayerId } from "../../types";
7
+
8
+ /**
9
+ * Task 15: Example Game Implementation - Coin Flip Game
10
+ *
11
+ * Simple game to validate the entire @drmxrcy/tcg-core framework:
12
+ * - Players take turns flipping a coin (using seeded RNG)
13
+ * - Heads = score +1, Tails = no score
14
+ * - First player to reach 3 points wins
15
+ *
16
+ * Tests verify:
17
+ * - Game setup and initialization
18
+ * - Turn-based gameplay with flow management
19
+ * - Move execution with RNG integration
20
+ * - Win condition checking
21
+ * - Complete game playthrough
22
+ * - Deterministic replay
23
+ */
24
+
25
+ type CoinFlipGameState = {
26
+ players: Array<{
27
+ id: PlayerId;
28
+ name: string;
29
+ score: number;
30
+ }>;
31
+ currentPlayerIndex: number;
32
+ turnNumber: number;
33
+ phase: "flip" | "ended";
34
+ lastFlipResult?: "heads" | "tails";
35
+ winner?: PlayerId;
36
+ };
37
+
38
+ type CoinFlipMoves = {
39
+ flipCoin: Record<string, never>;
40
+ endTurn: Record<string, never>;
41
+ };
42
+
43
+ describe("Coin Flip Game - Setup", () => {
44
+ describe("Task 15.1, 15.2: Game Definition and Setup", () => {
45
+ it("should create game definition for coin flip", () => {
46
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
47
+ flipCoin: {
48
+ condition: (state) => state.phase === "flip",
49
+ reducer: (draft, _context) => {
50
+ // Access RNG through context (we'll pass it from engine)
51
+ const isHeads = Math.random() >= 0.5; // Placeholder
52
+ draft.lastFlipResult = isHeads ? "heads" : "tails";
53
+
54
+ if (isHeads) {
55
+ const player = draft.players[draft.currentPlayerIndex];
56
+ if (player) {
57
+ player.score += 1;
58
+ }
59
+ }
60
+ },
61
+ },
62
+ endTurn: {
63
+ condition: (state) => state.phase === "flip",
64
+ reducer: (draft) => {
65
+ // Next player
66
+ draft.currentPlayerIndex =
67
+ (draft.currentPlayerIndex + 1) % draft.players.length;
68
+ draft.turnNumber += 1;
69
+ },
70
+ },
71
+ };
72
+
73
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
74
+ name: "Coin Flip",
75
+ setup: (players) => ({
76
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
77
+ id: p.id as PlayerId,
78
+ name: p.name || "Player",
79
+ score: 0,
80
+ })),
81
+ currentPlayerIndex: 0,
82
+ turnNumber: 1,
83
+ phase: "flip" as const,
84
+ }),
85
+ moves,
86
+ };
87
+
88
+ expect(gameDefinition.name).toBe("Coin Flip");
89
+ expect(gameDefinition.setup).toBeFunction();
90
+ expect(gameDefinition.moves.flipCoin).toBeDefined();
91
+ expect(gameDefinition.moves.endTurn).toBeDefined();
92
+ });
93
+
94
+ it("should initialize game state correctly", () => {
95
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
96
+ flipCoin: { reducer: () => {} },
97
+ endTurn: { reducer: () => {} },
98
+ };
99
+
100
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
101
+ name: "Coin Flip",
102
+ setup: (players) => ({
103
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
104
+ id: p.id as PlayerId,
105
+ name: p.name || "Player",
106
+ score: 0,
107
+ })),
108
+ currentPlayerIndex: 0,
109
+ turnNumber: 1,
110
+ phase: "flip" as const,
111
+ }),
112
+ moves,
113
+ };
114
+
115
+ const players = [
116
+ { id: createPlayerId("p1"), name: "Alice" },
117
+ { id: createPlayerId("p2"), name: "Bob" },
118
+ ];
119
+
120
+ const engine = new RuleEngine(gameDefinition, players);
121
+ const state = engine.getState();
122
+
123
+ expect(state.players).toHaveLength(2);
124
+ expect(state.players[0]?.name).toBe("Alice");
125
+ expect(state.players[1]?.name).toBe("Bob");
126
+ expect(state.currentPlayerIndex).toBe(0);
127
+ expect(state.turnNumber).toBe(1);
128
+ expect(state.phase).toBe("flip");
129
+ });
130
+ });
131
+
132
+ describe("Task 15.3, 15.4: Game Moves", () => {
133
+ it("should implement flipCoin move with RNG", () => {
134
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
135
+ flipCoin: {
136
+ reducer: (draft) => {
137
+ // In real implementation, would use engine.getRNG()
138
+ const isHeads = Math.random() >= 0.5;
139
+ draft.lastFlipResult = isHeads ? "heads" : "tails";
140
+
141
+ if (isHeads) {
142
+ const player = draft.players[draft.currentPlayerIndex];
143
+ if (player) {
144
+ player.score += 1;
145
+ }
146
+ }
147
+ },
148
+ },
149
+ endTurn: { reducer: () => {} },
150
+ };
151
+
152
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
153
+ name: "Coin Flip",
154
+ setup: (players) => ({
155
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
156
+ id: p.id as PlayerId,
157
+ name: p.name || "Player",
158
+ score: 0,
159
+ })),
160
+ currentPlayerIndex: 0,
161
+ turnNumber: 1,
162
+ phase: "flip" as const,
163
+ }),
164
+ moves,
165
+ };
166
+
167
+ const players = [
168
+ { id: createPlayerId("p1"), name: "Alice" },
169
+ { id: createPlayerId("p2"), name: "Bob" },
170
+ ];
171
+
172
+ const engine = new RuleEngine(gameDefinition, players, {
173
+ seed: "test-seed",
174
+ });
175
+
176
+ const result = engine.executeMove("flipCoin", {
177
+ playerId: createPlayerId("p1"),
178
+ params: {},
179
+ });
180
+
181
+ expect(result.success).toBe(true);
182
+
183
+ const state = engine.getState();
184
+ expect(state.lastFlipResult).toBeDefined();
185
+ expect(["heads", "tails"]).toContain(state.lastFlipResult);
186
+ });
187
+
188
+ it("should implement endTurn move to progress to next player", () => {
189
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
190
+ flipCoin: { reducer: () => {} },
191
+ endTurn: {
192
+ reducer: (draft) => {
193
+ draft.currentPlayerIndex =
194
+ (draft.currentPlayerIndex + 1) % draft.players.length;
195
+ draft.turnNumber += 1;
196
+ },
197
+ },
198
+ };
199
+
200
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
201
+ name: "Coin Flip",
202
+ setup: (players) => ({
203
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
204
+ id: p.id as PlayerId,
205
+ name: p.name || "Player",
206
+ score: 0,
207
+ })),
208
+ currentPlayerIndex: 0,
209
+ turnNumber: 1,
210
+ phase: "flip" as const,
211
+ }),
212
+ moves,
213
+ };
214
+
215
+ const players = [
216
+ { id: createPlayerId("p1"), name: "Alice" },
217
+ { id: createPlayerId("p2"), name: "Bob" },
218
+ ];
219
+
220
+ const engine = new RuleEngine(gameDefinition, players);
221
+
222
+ expect(engine.getState().currentPlayerIndex).toBe(0);
223
+
224
+ engine.executeMove("endTurn", {
225
+ playerId: createPlayerId("p1"),
226
+ params: {},
227
+ });
228
+
229
+ const state = engine.getState();
230
+ expect(state.currentPlayerIndex).toBe(1);
231
+ expect(state.turnNumber).toBe(2);
232
+ });
233
+ });
234
+
235
+ describe("Task 15.5, 15.6: Game Flow", () => {
236
+ it("should define turn-based flow", () => {
237
+ const flow: FlowDefinition<CoinFlipGameState> = {
238
+ turn: {
239
+ onBegin: (context) => {
240
+ context.state.phase = "flip";
241
+ },
242
+ phases: {
243
+ flip: {
244
+ order: 0,
245
+ next: undefined,
246
+ },
247
+ },
248
+ },
249
+ };
250
+
251
+ expect(flow.turn).toBeDefined();
252
+ expect(flow.turn.phases?.flip).toBeDefined();
253
+ });
254
+
255
+ it("should integrate flow with game", () => {
256
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
257
+ flipCoin: { reducer: () => {} },
258
+ endTurn: { reducer: () => {} },
259
+ };
260
+
261
+ const flow: FlowDefinition<CoinFlipGameState> = {
262
+ turn: {
263
+ onBegin: (context) => {
264
+ context.state.phase = "flip";
265
+ },
266
+ phases: {
267
+ flip: { order: 0, next: undefined },
268
+ },
269
+ },
270
+ };
271
+
272
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
273
+ name: "Coin Flip",
274
+ setup: (players) => ({
275
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
276
+ id: p.id as PlayerId,
277
+ name: p.name || "Player",
278
+ score: 0,
279
+ })),
280
+ currentPlayerIndex: 0,
281
+ turnNumber: 1,
282
+ phase: "flip" as const,
283
+ }),
284
+ moves,
285
+ flow,
286
+ };
287
+
288
+ const players = [
289
+ { id: createPlayerId("p1"), name: "Alice" },
290
+ { id: createPlayerId("p2"), name: "Bob" },
291
+ ];
292
+
293
+ const engine = new RuleEngine(gameDefinition, players);
294
+ const flowManager = engine.getFlowManager();
295
+
296
+ expect(flowManager).toBeDefined();
297
+ expect(flowManager?.getCurrentPhase()).toBe("flip");
298
+ });
299
+ });
300
+
301
+ describe("Task 15.7, 15.8: End Conditions", () => {
302
+ it("should define win condition (first to 3 points)", () => {
303
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
304
+ flipCoin: { reducer: () => {} },
305
+ endTurn: { reducer: () => {} },
306
+ };
307
+
308
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
309
+ name: "Coin Flip",
310
+ setup: (players) => ({
311
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
312
+ id: p.id as PlayerId,
313
+ name: p.name || "Player",
314
+ score: 0,
315
+ })),
316
+ currentPlayerIndex: 0,
317
+ turnNumber: 1,
318
+ phase: "flip" as const,
319
+ }),
320
+ moves,
321
+ endIf: (state) => {
322
+ const winner = state.players.find((p) => p.score >= 3);
323
+ if (winner) {
324
+ return {
325
+ winner: winner.id,
326
+ reason: "Reached 3 points",
327
+ };
328
+ }
329
+ return undefined;
330
+ },
331
+ };
332
+
333
+ const players = [
334
+ { id: createPlayerId("p1"), name: "Alice" },
335
+ { id: createPlayerId("p2"), name: "Bob" },
336
+ ];
337
+
338
+ const engine = new RuleEngine(gameDefinition, players);
339
+
340
+ // Game should not be ended yet
341
+ expect(engine.checkGameEnd()).toBeUndefined();
342
+ });
343
+
344
+ it("should detect game end when player reaches 3 points", () => {
345
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
346
+ flipCoin: {
347
+ reducer: (draft) => {
348
+ // Force heads for testing
349
+ draft.lastFlipResult = "heads";
350
+ const player = draft.players[draft.currentPlayerIndex];
351
+ if (player) {
352
+ player.score += 1;
353
+ }
354
+ },
355
+ },
356
+ endTurn: { reducer: () => {} },
357
+ };
358
+
359
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
360
+ name: "Coin Flip",
361
+ setup: (players) => ({
362
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
363
+ id: p.id as PlayerId,
364
+ name: p.name || "Player",
365
+ score: 0,
366
+ })),
367
+ currentPlayerIndex: 0,
368
+ turnNumber: 1,
369
+ phase: "flip" as const,
370
+ }),
371
+ moves,
372
+ endIf: (state) => {
373
+ const winner = state.players.find((p) => p.score >= 3);
374
+ if (winner) {
375
+ return {
376
+ winner: winner.id,
377
+ reason: "Reached 3 points",
378
+ };
379
+ }
380
+ return undefined;
381
+ },
382
+ };
383
+
384
+ const players = [
385
+ { id: createPlayerId("p1"), name: "Alice" },
386
+ { id: createPlayerId("p2"), name: "Bob" },
387
+ ];
388
+
389
+ const engine = new RuleEngine(gameDefinition, players);
390
+
391
+ // Flip 3 times to win
392
+ engine.executeMove("flipCoin", {
393
+ playerId: createPlayerId("p1"),
394
+ params: {},
395
+ });
396
+ engine.executeMove("flipCoin", {
397
+ playerId: createPlayerId("p1"),
398
+ params: {},
399
+ });
400
+ engine.executeMove("flipCoin", {
401
+ playerId: createPlayerId("p1"),
402
+ params: {},
403
+ });
404
+
405
+ const gameEnd = engine.checkGameEnd();
406
+ expect(gameEnd).toBeDefined();
407
+ expect(gameEnd?.winner).toBe(createPlayerId("p1"));
408
+ expect(gameEnd?.reason).toBe("Reached 3 points");
409
+ });
410
+ });
411
+
412
+ describe("Task 15.9, 15.10: Complete Game Playthrough", () => {
413
+ it("should play complete game from start to finish", () => {
414
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
415
+ flipCoin: {
416
+ reducer: (draft) => {
417
+ // Use deterministic "random" for test
418
+ const isHeads = Math.random() >= 0.5;
419
+ draft.lastFlipResult = isHeads ? "heads" : "tails";
420
+
421
+ if (isHeads) {
422
+ const player = draft.players[draft.currentPlayerIndex];
423
+ if (player) {
424
+ player.score += 1;
425
+ }
426
+ }
427
+ },
428
+ },
429
+ endTurn: {
430
+ reducer: (draft) => {
431
+ draft.currentPlayerIndex =
432
+ (draft.currentPlayerIndex + 1) % draft.players.length;
433
+ draft.turnNumber += 1;
434
+ },
435
+ },
436
+ };
437
+
438
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
439
+ name: "Coin Flip",
440
+ setup: (players) => ({
441
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
442
+ id: p.id as PlayerId,
443
+ name: p.name || "Player",
444
+ score: 0,
445
+ })),
446
+ currentPlayerIndex: 0,
447
+ turnNumber: 1,
448
+ phase: "flip" as const,
449
+ }),
450
+ moves,
451
+ endIf: (state) => {
452
+ const winner = state.players.find((p) => p.score >= 3);
453
+ if (winner) {
454
+ return {
455
+ winner: winner.id,
456
+ reason: "Reached 3 points",
457
+ };
458
+ }
459
+ return undefined;
460
+ },
461
+ };
462
+
463
+ const players = [
464
+ { id: createPlayerId("p1"), name: "Alice" },
465
+ { id: createPlayerId("p2"), name: "Bob" },
466
+ ];
467
+
468
+ const engine = new RuleEngine(gameDefinition, players, {
469
+ seed: "playthrough-test",
470
+ });
471
+
472
+ let gameEnd = engine.checkGameEnd();
473
+ let turn = 0;
474
+ const maxTurns = 50; // Safety limit
475
+
476
+ // Play until someone wins or max turns reached
477
+ while (!gameEnd && turn < maxTurns) {
478
+ const currentPlayer =
479
+ engine.getState().players[engine.getState().currentPlayerIndex];
480
+
481
+ // Flip coin
482
+ if (currentPlayer?.id) {
483
+ engine.executeMove("flipCoin", {
484
+ playerId: currentPlayer.id,
485
+ params: {},
486
+ });
487
+
488
+ // End turn
489
+ engine.executeMove("endTurn", {
490
+ playerId: currentPlayer.id,
491
+ params: {},
492
+ });
493
+ }
494
+
495
+ gameEnd = engine.checkGameEnd();
496
+ turn++;
497
+ }
498
+
499
+ // Game should have ended
500
+ expect(gameEnd).toBeDefined();
501
+ expect(turn).toBeLessThan(maxTurns);
502
+
503
+ // Winner should have at least 3 points
504
+ const finalState = engine.getState();
505
+ const winningPlayer = finalState.players.find(
506
+ (p) => p.id === gameEnd?.winner,
507
+ );
508
+ expect(winningPlayer?.score).toBeGreaterThanOrEqual(3);
509
+ });
510
+ });
511
+
512
+ describe("Task 15.11, 15.12: Deterministic Replay", () => {
513
+ it("should support undo/redo for game history", () => {
514
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
515
+ flipCoin: {
516
+ reducer: (draft) => {
517
+ // Force heads for deterministic test
518
+ draft.lastFlipResult = "heads";
519
+ const player = draft.players[draft.currentPlayerIndex];
520
+ if (player) {
521
+ player.score += 1;
522
+ }
523
+ },
524
+ },
525
+ endTurn: {
526
+ reducer: (draft) => {
527
+ draft.currentPlayerIndex =
528
+ (draft.currentPlayerIndex + 1) % draft.players.length;
529
+ draft.turnNumber += 1;
530
+ },
531
+ },
532
+ };
533
+
534
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
535
+ name: "Coin Flip",
536
+ setup: (players) => ({
537
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
538
+ id: p.id as PlayerId,
539
+ name: p.name || "Player",
540
+ score: 0,
541
+ })),
542
+ currentPlayerIndex: 0,
543
+ turnNumber: 1,
544
+ phase: "flip" as const,
545
+ }),
546
+ moves,
547
+ };
548
+
549
+ const players = [
550
+ { id: createPlayerId("p1"), name: "Alice" },
551
+ { id: createPlayerId("p2"), name: "Bob" },
552
+ ];
553
+
554
+ const engine = new RuleEngine(gameDefinition, players);
555
+
556
+ // Execute some moves
557
+ engine.executeMove("flipCoin", {
558
+ playerId: createPlayerId("p1"),
559
+ params: {},
560
+ });
561
+ engine.executeMove("endTurn", {
562
+ playerId: createPlayerId("p1"),
563
+ params: {},
564
+ });
565
+ engine.executeMove("flipCoin", {
566
+ playerId: createPlayerId("p2"),
567
+ params: {},
568
+ });
569
+
570
+ expect(engine.getState().players[0]?.score).toBe(1);
571
+ expect(engine.getState().players[1]?.score).toBe(1);
572
+
573
+ // Undo last move
574
+ engine.undo();
575
+ expect(engine.getState().players[1]?.score).toBe(0);
576
+
577
+ // Redo
578
+ engine.redo();
579
+ expect(engine.getState().players[1]?.score).toBe(1);
580
+
581
+ // History tracking
582
+ const history = engine.getHistory();
583
+ expect(history.length).toBe(3);
584
+ });
585
+
586
+ it("should track patches for network synchronization", () => {
587
+ const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
588
+ flipCoin: {
589
+ reducer: (draft) => {
590
+ draft.lastFlipResult = "heads";
591
+ const player = draft.players[draft.currentPlayerIndex];
592
+ if (player) {
593
+ player.score += 1;
594
+ }
595
+ },
596
+ },
597
+ endTurn: { reducer: () => {} },
598
+ };
599
+
600
+ const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
601
+ name: "Coin Flip",
602
+ setup: (players) => ({
603
+ players: players.map((p): CoinFlipGameState["players"][number] => ({
604
+ id: p.id as PlayerId,
605
+ name: p.name || "Player",
606
+ score: 0,
607
+ })),
608
+ currentPlayerIndex: 0,
609
+ turnNumber: 1,
610
+ phase: "flip" as const,
611
+ }),
612
+ moves,
613
+ };
614
+
615
+ const players = [
616
+ { id: createPlayerId("p1"), name: "Alice" },
617
+ { id: createPlayerId("p2"), name: "Bob" },
618
+ ];
619
+
620
+ const engine = new RuleEngine(gameDefinition, players);
621
+
622
+ // Execute move and capture patches
623
+ const result = engine.executeMove("flipCoin", {
624
+ playerId: createPlayerId("p1"),
625
+ params: {},
626
+ });
627
+
628
+ expect(result.success).toBe(true);
629
+ if (result.success) {
630
+ // Patches should be captured for network sync
631
+ expect(result.patches).toBeDefined();
632
+ expect(result.patches.length).toBeGreaterThan(0);
633
+ expect(result.inversePatches).toBeDefined();
634
+
635
+ // Get accumulated patches
636
+ const allPatches = engine.getPatches();
637
+ expect(allPatches.length).toBeGreaterThan(0);
638
+ }
639
+ });
640
+ });
641
+ });