@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,389 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { GameDefinition } from "../game-definition/game-definition";
3
+ import type { GameMoveDefinitions } from "../game-definition/move-definitions";
4
+ import { createPlayerId } from "../types";
5
+ import { createTestEngine } from "./test-engine-builder";
6
+ import { createTestPlayers } from "./test-player-builder";
7
+
8
+ /**
9
+ * Tests for createTestEngine - Engine builder for tests
10
+ *
11
+ * Task 2.1: Write tests for test builders (createTestEngine)
12
+ *
13
+ * Tests verify:
14
+ * - Creating engine with minimal game definition
15
+ * - Creating engine with custom players
16
+ * - Creating engine with options (seed)
17
+ * - Default player generation
18
+ * - Engine is properly initialized and functional
19
+ */
20
+
21
+ type SimpleGameState = {
22
+ players: Array<{ id: string; name: string; score: number }>;
23
+ currentPlayer: number;
24
+ };
25
+
26
+ type SimpleMoves = {
27
+ incrementScore: Record<string, never>;
28
+ pass: Record<string, never>;
29
+ };
30
+
31
+ describe("createTestEngine", () => {
32
+ describe("Basic Functionality", () => {
33
+ it("should create engine with minimal game definition and default players", () => {
34
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
35
+ incrementScore: {
36
+ reducer: (draft: SimpleGameState) => {
37
+ const player = draft.players[draft.currentPlayer];
38
+ if (player) {
39
+ player.score += 1;
40
+ }
41
+ },
42
+ },
43
+ pass: {
44
+ reducer: (draft: SimpleGameState) => {
45
+ draft.currentPlayer =
46
+ (draft.currentPlayer + 1) % draft.players.length;
47
+ },
48
+ },
49
+ };
50
+
51
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
52
+ name: "Test Game",
53
+ setup: (players) => ({
54
+ players: players.map((p) => ({
55
+ id: p.id,
56
+ name: p.name || "Player",
57
+ score: 0,
58
+ })),
59
+ currentPlayer: 0,
60
+ }),
61
+ moves,
62
+ };
63
+
64
+ const engine = createTestEngine(definition);
65
+
66
+ // Engine should be initialized
67
+ expect(engine).toBeDefined();
68
+
69
+ // Should have default 2 players
70
+ const state = engine.getState();
71
+ expect(state.players).toHaveLength(2);
72
+ expect(state.players[0]?.name).toBe("Player 1");
73
+ expect(state.players[1]?.name).toBe("Player 2");
74
+ });
75
+
76
+ it("should create engine with custom players", () => {
77
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
78
+ incrementScore: { reducer: () => {} },
79
+ pass: { reducer: () => {} },
80
+ };
81
+
82
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
83
+ name: "Test Game",
84
+ setup: (players) => ({
85
+ players: players.map((p) => ({
86
+ id: p.id,
87
+ name: p.name || "Player",
88
+ score: 0,
89
+ })),
90
+ currentPlayer: 0,
91
+ }),
92
+ moves,
93
+ };
94
+
95
+ const customPlayers = createTestPlayers(3, ["Alice", "Bob", "Charlie"]);
96
+ const engine = createTestEngine(definition, customPlayers);
97
+
98
+ const state = engine.getState();
99
+ expect(state.players).toHaveLength(3);
100
+ expect(state.players[0]?.name).toBe("Alice");
101
+ expect(state.players[1]?.name).toBe("Bob");
102
+ expect(state.players[2]?.name).toBe("Charlie");
103
+ });
104
+
105
+ it("should create engine with seed option for deterministic RNG", () => {
106
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
107
+ incrementScore: { reducer: () => {} },
108
+ pass: { reducer: () => {} },
109
+ };
110
+
111
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
112
+ name: "Test Game",
113
+ setup: (players) => ({
114
+ players: players.map((p) => ({
115
+ id: p.id,
116
+ name: p.name || "Player",
117
+ score: 0,
118
+ })),
119
+ currentPlayer: 0,
120
+ }),
121
+ moves,
122
+ };
123
+
124
+ const engine = createTestEngine(definition, undefined, {
125
+ seed: "test-seed",
126
+ });
127
+
128
+ // Engine should have seeded RNG
129
+ expect(engine.getRNG()).toBeDefined();
130
+ expect(engine.getRNG().getSeed()).toBe("test-seed");
131
+ });
132
+ });
133
+
134
+ describe("Engine Functionality", () => {
135
+ it("should create functional engine that can execute moves", () => {
136
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
137
+ incrementScore: {
138
+ reducer: (draft: SimpleGameState) => {
139
+ const player = draft.players[draft.currentPlayer];
140
+ if (player) {
141
+ player.score += 1;
142
+ }
143
+ },
144
+ },
145
+ pass: { reducer: () => {} },
146
+ };
147
+
148
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
149
+ name: "Test Game",
150
+ setup: (players) => ({
151
+ players: players.map((p) => ({
152
+ id: p.id,
153
+ name: p.name || "Player",
154
+ score: 0,
155
+ })),
156
+ currentPlayer: 0,
157
+ }),
158
+ moves,
159
+ };
160
+
161
+ const engine = createTestEngine(definition);
162
+
163
+ const result = engine.executeMove("incrementScore", {
164
+ playerId: createPlayerId("test-p1"),
165
+ params: {},
166
+ });
167
+
168
+ expect(result.success).toBe(true);
169
+
170
+ const state = engine.getState();
171
+ expect(state.players[0]?.score).toBe(1);
172
+ });
173
+
174
+ it("should create engine with history tracking", () => {
175
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
176
+ incrementScore: {
177
+ reducer: (draft: SimpleGameState) => {
178
+ const player = draft.players[draft.currentPlayer];
179
+ if (player) {
180
+ player.score += 1;
181
+ }
182
+ },
183
+ },
184
+ pass: { reducer: () => {} },
185
+ };
186
+
187
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
188
+ name: "Test Game",
189
+ setup: (players) => ({
190
+ players: players.map((p) => ({
191
+ id: p.id,
192
+ name: p.name || "Player",
193
+ score: 0,
194
+ })),
195
+ currentPlayer: 0,
196
+ }),
197
+ moves,
198
+ };
199
+
200
+ const engine = createTestEngine(definition);
201
+
202
+ engine.executeMove("incrementScore", {
203
+ playerId: createPlayerId("test-p1"),
204
+ params: {},
205
+ });
206
+ engine.executeMove("incrementScore", {
207
+ playerId: createPlayerId("test-p1"),
208
+ params: {},
209
+ });
210
+
211
+ const history = engine.getHistory();
212
+ expect(history.length).toBe(2);
213
+ });
214
+
215
+ it("should create engine that supports undo/redo", () => {
216
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
217
+ incrementScore: {
218
+ reducer: (draft: SimpleGameState) => {
219
+ const player = draft.players[draft.currentPlayer];
220
+ if (player) {
221
+ player.score += 1;
222
+ }
223
+ },
224
+ },
225
+ pass: { reducer: () => {} },
226
+ };
227
+
228
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
229
+ name: "Test Game",
230
+ setup: (players) => ({
231
+ players: players.map((p) => ({
232
+ id: p.id,
233
+ name: p.name || "Player",
234
+ score: 0,
235
+ })),
236
+ currentPlayer: 0,
237
+ }),
238
+ moves,
239
+ };
240
+
241
+ const engine = createTestEngine(definition);
242
+
243
+ engine.executeMove("incrementScore", {
244
+ playerId: createPlayerId("test-p1"),
245
+ params: {},
246
+ });
247
+ expect(engine.getState().players[0]?.score).toBe(1);
248
+
249
+ engine.undo();
250
+ expect(engine.getState().players[0]?.score).toBe(0);
251
+
252
+ engine.redo();
253
+ expect(engine.getState().players[0]?.score).toBe(1);
254
+ });
255
+ });
256
+
257
+ describe("Options", () => {
258
+ it("should accept undefined players and use defaults", () => {
259
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
260
+ incrementScore: { reducer: () => {} },
261
+ pass: { reducer: () => {} },
262
+ };
263
+
264
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
265
+ name: "Test Game",
266
+ setup: (players) => ({
267
+ players: players.map((p) => ({
268
+ id: p.id,
269
+ name: p.name || "Player",
270
+ score: 0,
271
+ })),
272
+ currentPlayer: 0,
273
+ }),
274
+ moves,
275
+ };
276
+
277
+ const engine = createTestEngine(definition, undefined);
278
+
279
+ const state = engine.getState();
280
+ expect(state.players).toHaveLength(2);
281
+ });
282
+
283
+ it("should accept undefined options", () => {
284
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
285
+ incrementScore: { reducer: () => {} },
286
+ pass: { reducer: () => {} },
287
+ };
288
+
289
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
290
+ name: "Test Game",
291
+ setup: (players) => ({
292
+ players: players.map((p) => ({
293
+ id: p.id,
294
+ name: p.name || "Player",
295
+ score: 0,
296
+ })),
297
+ currentPlayer: 0,
298
+ }),
299
+ moves,
300
+ };
301
+
302
+ const engine = createTestEngine(definition, undefined, undefined);
303
+
304
+ expect(engine).toBeDefined();
305
+ expect(engine.getRNG()).toBeDefined();
306
+ });
307
+
308
+ it("should merge custom options with defaults", () => {
309
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
310
+ incrementScore: { reducer: () => {} },
311
+ pass: { reducer: () => {} },
312
+ };
313
+
314
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
315
+ name: "Test Game",
316
+ setup: (players) => ({
317
+ players: players.map((p) => ({
318
+ id: p.id,
319
+ name: p.name || "Player",
320
+ score: 0,
321
+ })),
322
+ currentPlayer: 0,
323
+ }),
324
+ moves,
325
+ };
326
+
327
+ const engine = createTestEngine(definition, undefined, {
328
+ seed: "custom-seed",
329
+ });
330
+
331
+ expect(engine.getRNG().getSeed()).toBe("custom-seed");
332
+ });
333
+ });
334
+
335
+ describe("Edge Cases", () => {
336
+ it("should create engine with single player", () => {
337
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
338
+ incrementScore: { reducer: () => {} },
339
+ pass: { reducer: () => {} },
340
+ };
341
+
342
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
343
+ name: "Test Game",
344
+ setup: (players) => ({
345
+ players: players.map((p) => ({
346
+ id: p.id,
347
+ name: p.name || "Player",
348
+ score: 0,
349
+ })),
350
+ currentPlayer: 0,
351
+ }),
352
+ moves,
353
+ };
354
+
355
+ const singlePlayer = createTestPlayers(1, ["Solo"]);
356
+ const engine = createTestEngine(definition, singlePlayer);
357
+
358
+ const state = engine.getState();
359
+ expect(state.players).toHaveLength(1);
360
+ expect(state.players[0]?.name).toBe("Solo");
361
+ });
362
+
363
+ it("should create engine with many players", () => {
364
+ const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
365
+ incrementScore: { reducer: () => {} },
366
+ pass: { reducer: () => {} },
367
+ };
368
+
369
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
370
+ name: "Test Game",
371
+ setup: (players) => ({
372
+ players: players.map((p) => ({
373
+ id: p.id,
374
+ name: p.name || "Player",
375
+ score: 0,
376
+ })),
377
+ currentPlayer: 0,
378
+ }),
379
+ moves,
380
+ };
381
+
382
+ const manyPlayers = createTestPlayers(6);
383
+ const engine = createTestEngine(definition, manyPlayers);
384
+
385
+ const state = engine.getState();
386
+ expect(state.players).toHaveLength(6);
387
+ });
388
+ });
389
+ });
@@ -0,0 +1,46 @@
1
+ import { RuleEngine, type RuleEngineOptions } from "../engine/rule-engine";
2
+ import type {
3
+ GameDefinition,
4
+ Player,
5
+ } from "../game-definition/game-definition";
6
+ import { createTestPlayers } from "./test-player-builder";
7
+
8
+ /**
9
+ * Test Engine Builder
10
+ *
11
+ * Task 2.3: Implement createTestEngine(definition, players?, options?)
12
+ *
13
+ * Creates a fully initialized RuleEngine for testing.
14
+ * Simplifies test setup by providing sensible defaults for players and options.
15
+ *
16
+ * Features:
17
+ * - Default 2 players if not provided
18
+ * - Optional seed for deterministic testing
19
+ * - Returns ready-to-use engine with initialized state
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Create engine with defaults (2 players)
24
+ * const engine = createTestEngine(gameDefinition);
25
+ *
26
+ * // Create engine with custom players
27
+ * const players = createTestPlayers(4, ['Alice', 'Bob', 'Charlie', 'Dave']);
28
+ * const engine = createTestEngine(gameDefinition, players);
29
+ *
30
+ * // Create engine with seed for deterministic tests
31
+ * const engine = createTestEngine(gameDefinition, undefined, {
32
+ * seed: 'test-seed-123'
33
+ * });
34
+ * ```
35
+ */
36
+ export function createTestEngine<TState, TMoves extends Record<string, any>>(
37
+ definition: GameDefinition<TState, TMoves>,
38
+ players?: Player[],
39
+ options?: RuleEngineOptions,
40
+ ): RuleEngine<TState, TMoves> {
41
+ // Use provided players or create default 2 players
42
+ const enginePlayers = players ?? createTestPlayers(2);
43
+
44
+ // Create and return engine with options
45
+ return new RuleEngine(definition, enginePlayers, options);
46
+ }
@@ -0,0 +1,284 @@
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
+ import { expectPhaseTransition } from "./test-flow-assertions";
8
+
9
+ /**
10
+ * Test state for flow assertions
11
+ */
12
+ type FlowTestState = {
13
+ players: Array<{
14
+ id: PlayerId;
15
+ name: string;
16
+ }>;
17
+ phase: "draw" | "main" | "end";
18
+ turnNumber: number;
19
+ };
20
+
21
+ type FlowTestMoves = {
22
+ nextPhase: Record<string, never>;
23
+ };
24
+
25
+ describe("test-flow-assertions", () => {
26
+ function createTestEngine() {
27
+ const moves: GameMoveDefinitions<FlowTestState, FlowTestMoves> = {
28
+ nextPhase: {
29
+ reducer: (draft) => {
30
+ // Transition through phases
31
+ if (draft.phase === "draw") {
32
+ draft.phase = "main";
33
+ } else if (draft.phase === "main") {
34
+ draft.phase = "end";
35
+ } else if (draft.phase === "end") {
36
+ draft.phase = "draw";
37
+ draft.turnNumber += 1;
38
+ }
39
+ },
40
+ },
41
+ };
42
+
43
+ const flow: FlowDefinition<FlowTestState> = {
44
+ turn: {
45
+ onBegin: (context) => {
46
+ context.state.phase = "draw";
47
+ },
48
+ phases: {
49
+ draw: {
50
+ order: 0,
51
+ next: "main",
52
+ },
53
+ main: {
54
+ order: 1,
55
+ next: "end",
56
+ },
57
+ end: {
58
+ order: 2,
59
+ next: undefined,
60
+ },
61
+ },
62
+ },
63
+ };
64
+
65
+ const gameDefinition: GameDefinition<FlowTestState, FlowTestMoves> = {
66
+ name: "Flow Test Game",
67
+ setup: (players) => ({
68
+ players: players.map((p) => ({
69
+ id: p.id as PlayerId,
70
+ name: p.name || "Player",
71
+ })),
72
+ phase: "draw" as const,
73
+ turnNumber: 1,
74
+ }),
75
+ moves,
76
+ flow,
77
+ };
78
+
79
+ const players = [
80
+ { id: createPlayerId("p1"), name: "Alice" },
81
+ { id: createPlayerId("p2"), name: "Bob" },
82
+ ];
83
+
84
+ return new RuleEngine(gameDefinition, players);
85
+ }
86
+
87
+ describe("expectPhaseTransition", () => {
88
+ it("should pass when phase transitions correctly", () => {
89
+ const engine = createTestEngine();
90
+
91
+ // Should not throw
92
+ expectPhaseTransition(
93
+ engine,
94
+ "nextPhase",
95
+ { playerId: createPlayerId("p1"), params: {} },
96
+ "draw",
97
+ "main",
98
+ );
99
+ });
100
+
101
+ it("should throw when initial phase does not match", () => {
102
+ const engine = createTestEngine();
103
+
104
+ expect(() => {
105
+ expectPhaseTransition(
106
+ engine,
107
+ "nextPhase",
108
+ { playerId: createPlayerId("p1"), params: {} },
109
+ "main", // Wrong initial phase
110
+ "end",
111
+ );
112
+ }).toThrow(/Expected initial phase to be 'main'/);
113
+ });
114
+
115
+ it("should throw when final phase does not match", () => {
116
+ const engine = createTestEngine();
117
+
118
+ expect(() => {
119
+ expectPhaseTransition(
120
+ engine,
121
+ "nextPhase",
122
+ { playerId: createPlayerId("p1"), params: {} },
123
+ "draw",
124
+ "end", // Wrong final phase (should be 'main')
125
+ );
126
+ }).toThrow(/Expected final phase to be 'end'/);
127
+ });
128
+
129
+ it("should work with multiple transitions", () => {
130
+ const engine = createTestEngine();
131
+
132
+ expectPhaseTransition(
133
+ engine,
134
+ "nextPhase",
135
+ { playerId: createPlayerId("p1"), params: {} },
136
+ "draw",
137
+ "main",
138
+ );
139
+
140
+ expectPhaseTransition(
141
+ engine,
142
+ "nextPhase",
143
+ { playerId: createPlayerId("p1"), params: {} },
144
+ "main",
145
+ "end",
146
+ );
147
+
148
+ expectPhaseTransition(
149
+ engine,
150
+ "nextPhase",
151
+ { playerId: createPlayerId("p1"), params: {} },
152
+ "end",
153
+ "draw",
154
+ );
155
+ });
156
+
157
+ it("should handle move failures", () => {
158
+ const _engine = createTestEngine();
159
+
160
+ // Create a move that will fail
161
+ const moves: GameMoveDefinitions<FlowTestState, FlowTestMoves> = {
162
+ nextPhase: {
163
+ condition: () => false, // Always fails
164
+ reducer: () => {},
165
+ },
166
+ };
167
+
168
+ const flow: FlowDefinition<FlowTestState> = {
169
+ turn: {
170
+ phases: {
171
+ draw: { order: 0, next: "main" },
172
+ main: { order: 1, next: undefined },
173
+ },
174
+ },
175
+ };
176
+
177
+ const gameDefinition: GameDefinition<FlowTestState, FlowTestMoves> = {
178
+ name: "Failing Flow Test",
179
+ setup: (players) => ({
180
+ players: players.map((p) => ({
181
+ id: p.id as PlayerId,
182
+ name: p.name || "Player",
183
+ })),
184
+ phase: "draw" as const,
185
+ turnNumber: 1,
186
+ }),
187
+ moves,
188
+ flow,
189
+ };
190
+
191
+ const failEngine = new RuleEngine(gameDefinition, [
192
+ { id: createPlayerId("p1"), name: "Alice" },
193
+ ]);
194
+
195
+ expect(() => {
196
+ expectPhaseTransition(
197
+ failEngine,
198
+ "nextPhase",
199
+ { playerId: createPlayerId("p1"), params: {} },
200
+ "draw",
201
+ "main",
202
+ );
203
+ }).toThrow(/Move failed/);
204
+ });
205
+
206
+ it("should work with FlowManager integration", () => {
207
+ const engine = createTestEngine();
208
+ const flowManager = engine.getFlowManager();
209
+
210
+ expect(flowManager).toBeDefined();
211
+ expect(flowManager?.getCurrentPhase()).toBe("draw");
212
+
213
+ // Transition phase
214
+ expectPhaseTransition(
215
+ engine,
216
+ "nextPhase",
217
+ { playerId: createPlayerId("p1"), params: {} },
218
+ "draw",
219
+ "main",
220
+ );
221
+
222
+ // Note: FlowManager tracks its own phase state separate from game state
223
+ // This test verifies that expectPhaseTransition works with engines that have FlowManager
224
+ const state = engine.getState();
225
+ expect(state.phase).toBe("main");
226
+ });
227
+
228
+ it("should accept phase path for nested state", () => {
229
+ // Create engine with nested phase in state
230
+ type NestedFlowState = {
231
+ players: Array<{ id: PlayerId; name: string }>;
232
+ gameState: {
233
+ currentPhase: "start" | "middle" | "end";
234
+ };
235
+ turnNumber: number;
236
+ };
237
+
238
+ type NestedMoves = {
239
+ advance: Record<string, never>;
240
+ };
241
+
242
+ const moves: GameMoveDefinitions<NestedFlowState, NestedMoves> = {
243
+ advance: {
244
+ reducer: (draft) => {
245
+ if (draft.gameState.currentPhase === "start") {
246
+ draft.gameState.currentPhase = "middle";
247
+ } else if (draft.gameState.currentPhase === "middle") {
248
+ draft.gameState.currentPhase = "end";
249
+ }
250
+ },
251
+ },
252
+ };
253
+
254
+ const gameDefinition: GameDefinition<NestedFlowState, NestedMoves> = {
255
+ name: "Nested Flow Test",
256
+ setup: (players) => ({
257
+ players: players.map((p) => ({
258
+ id: p.id as PlayerId,
259
+ name: p.name || "Player",
260
+ })),
261
+ gameState: {
262
+ currentPhase: "start" as const,
263
+ },
264
+ turnNumber: 1,
265
+ }),
266
+ moves,
267
+ };
268
+
269
+ const nestedEngine = new RuleEngine(gameDefinition, [
270
+ { id: createPlayerId("p1"), name: "Alice" },
271
+ ]);
272
+
273
+ // Should work with custom phase path
274
+ expectPhaseTransition(
275
+ nestedEngine,
276
+ "advance",
277
+ { playerId: createPlayerId("p1"), params: {} },
278
+ "start",
279
+ "middle",
280
+ "gameState.currentPhase",
281
+ );
282
+ });
283
+ });
284
+ });