@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,35 @@
1
+ /**
2
+ * Task 9: Flow Manager
3
+ *
4
+ * Flow orchestration system for turn/phase/segment management.
5
+ *
6
+ * Key features:
7
+ * - Rich FlowContext API (not just state)
8
+ * - Programmatic and automatic transitions
9
+ * - Configurable progression logic
10
+ * - Hierarchical states (turn → phases → segments)
11
+ * - Lifecycle hooks at all levels
12
+ *
13
+ * Note: Originally planned to use XState, but a simple state machine
14
+ * proved more appropriate. No external dependencies needed.
15
+ *
16
+ * @module flow
17
+ */
18
+
19
+ export type {
20
+ EndCondition,
21
+ FlowContext,
22
+ FlowDefinition,
23
+ GameSegmentDefinition,
24
+ LifecycleHook,
25
+ PhaseDefinition,
26
+ StepDefinition,
27
+ TurnDefinition,
28
+ } from "./flow-definition";
29
+
30
+ export {
31
+ FlowManager,
32
+ type FlowManagerOptions,
33
+ type FlowStateSnapshot,
34
+ type SerializedFlowState,
35
+ } from "./flow-manager";
@@ -0,0 +1,359 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { GameDefinition } from "../game-definition";
3
+ import type { GameMoveDefinitions } from "../move-definitions";
4
+ import { validateGameDefinition } from "../validation";
5
+
6
+ /**
7
+ * Test suite for GameDefinition validation
8
+ *
9
+ * Task 10.3, 10.7, 10.9, 10.11, 10.13: Write tests for validation
10
+ *
11
+ * Validates:
12
+ * - Setup function validation
13
+ * - Flow configuration validation
14
+ * - EndIf condition validation
15
+ * - PlayerView function validation
16
+ * - Zod schema validation
17
+ */
18
+
19
+ type SimpleGameState = {
20
+ value: number;
21
+ phase: string;
22
+ };
23
+
24
+ type SimpleMoves = {
25
+ increment: Record<string, never>;
26
+ decrement: Record<string, never>;
27
+ };
28
+
29
+ describe("GameDefinition - Validation", () => {
30
+ describe("setup function validation (Task 10.3)", () => {
31
+ it("should validate that setup function exists", () => {
32
+ const definition = {
33
+ name: "Test Game",
34
+ setup: (players: Array<{ id: string; name?: string }>) => ({
35
+ value: players.length,
36
+ phase: "start",
37
+ }),
38
+ moves: {} as GameMoveDefinitions<SimpleGameState, SimpleMoves>,
39
+ };
40
+
41
+ const result = validateGameDefinition(definition);
42
+ expect(result.success).toBe(true);
43
+ });
44
+
45
+ it("should reject definition without setup function", () => {
46
+ const definition = {
47
+ name: "Test Game",
48
+ moves: {} as GameMoveDefinitions<SimpleGameState, SimpleMoves>,
49
+ } as unknown as GameDefinition<SimpleGameState, SimpleMoves>;
50
+
51
+ const result = validateGameDefinition(definition);
52
+ expect(result.success).toBe(false);
53
+ if (!result.success) {
54
+ expect(result.error).toContain("setup");
55
+ }
56
+ });
57
+
58
+ it("should validate that setup function is callable", () => {
59
+ const definition = {
60
+ name: "Test Game",
61
+ setup: "not a function" as unknown,
62
+ moves: {} as GameMoveDefinitions<SimpleGameState, SimpleMoves>,
63
+ } as GameDefinition<SimpleGameState, SimpleMoves>;
64
+
65
+ const result = validateGameDefinition(definition);
66
+ expect(result.success).toBe(false);
67
+ if (!result.success) {
68
+ expect(result.error).toContain("setup");
69
+ expect(result.error).toContain("function");
70
+ }
71
+ });
72
+ });
73
+
74
+ describe("moves mapping validation (Task 10.5)", () => {
75
+ it("should validate that moves object exists", () => {
76
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
77
+ name: "Test Game",
78
+ setup: () => ({ value: 0, phase: "start" }),
79
+ moves: {
80
+ increment: {
81
+ reducer: (draft) => {
82
+ draft.value += 1;
83
+ },
84
+ },
85
+ decrement: {
86
+ reducer: (draft) => {
87
+ draft.value -= 1;
88
+ },
89
+ },
90
+ },
91
+ };
92
+
93
+ const result = validateGameDefinition(definition);
94
+ expect(result.success).toBe(true);
95
+ });
96
+
97
+ it("should reject definition without moves", () => {
98
+ const definition = {
99
+ name: "Test Game",
100
+ setup: () => ({ value: 0, phase: "start" }),
101
+ };
102
+
103
+ const result = validateGameDefinition(definition as any);
104
+ expect(result.success).toBe(false);
105
+ if (!result.success) {
106
+ expect(result.error).toContain("moves");
107
+ }
108
+ });
109
+
110
+ it("should validate that each move has a reducer", () => {
111
+ const definition = {
112
+ name: "Test Game",
113
+ setup: () => ({ value: 0, phase: "start" }),
114
+ moves: {
115
+ increment: {}, // Missing reducer
116
+ decrement: {},
117
+ },
118
+ };
119
+
120
+ const result = validateGameDefinition(definition as any);
121
+ expect(result.success).toBe(false);
122
+ if (!result.success) {
123
+ expect(result.error).toContain("reducer");
124
+ }
125
+ });
126
+ });
127
+
128
+ describe("endIf validation (Task 10.9)", () => {
129
+ it("should accept optional endIf function", () => {
130
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
131
+ name: "Test Game",
132
+ setup: () => ({ value: 0, phase: "start" }),
133
+ moves: {
134
+ increment: {
135
+ reducer: (draft) => {
136
+ draft.value += 1;
137
+ },
138
+ },
139
+ decrement: {
140
+ reducer: (draft) => {
141
+ draft.value -= 1;
142
+ },
143
+ },
144
+ },
145
+ endIf: (state) => {
146
+ if (state.value >= 10) {
147
+ return { winner: "player1", reason: "reached-10" };
148
+ }
149
+ return undefined;
150
+ },
151
+ };
152
+
153
+ const result = validateGameDefinition(definition);
154
+ expect(result.success).toBe(true);
155
+ });
156
+
157
+ it("should work without endIf function", () => {
158
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
159
+ name: "Test Game",
160
+ setup: () => ({ value: 0, phase: "start" }),
161
+ moves: {
162
+ increment: {
163
+ reducer: (draft) => {
164
+ draft.value += 1;
165
+ },
166
+ },
167
+ decrement: {
168
+ reducer: (draft) => {
169
+ draft.value -= 1;
170
+ },
171
+ },
172
+ },
173
+ };
174
+
175
+ const result = validateGameDefinition(definition);
176
+ expect(result.success).toBe(true);
177
+ });
178
+
179
+ it("should reject non-function endIf", () => {
180
+ const definition = {
181
+ name: "Test Game",
182
+ setup: () => ({ value: 0, phase: "start" }),
183
+ moves: {
184
+ increment: {
185
+ reducer: (draft: SimpleGameState) => {
186
+ draft.value += 1;
187
+ },
188
+ },
189
+ decrement: {
190
+ reducer: (draft: SimpleGameState) => {
191
+ draft.value -= 1;
192
+ },
193
+ },
194
+ },
195
+ endIf: "not a function",
196
+ };
197
+
198
+ const result = validateGameDefinition(definition as any);
199
+ expect(result.success).toBe(false);
200
+ if (!result.success) {
201
+ expect(result.error).toContain("endIf");
202
+ expect(result.error).toContain("function");
203
+ }
204
+ });
205
+ });
206
+
207
+ describe("playerView validation (Task 10.11)", () => {
208
+ it("should accept optional playerView function", () => {
209
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
210
+ name: "Test Game",
211
+ setup: () => ({ value: 0, phase: "start" }),
212
+ moves: {
213
+ increment: {
214
+ reducer: (draft) => {
215
+ draft.value += 1;
216
+ },
217
+ },
218
+ decrement: {
219
+ reducer: (draft) => {
220
+ draft.value -= 1;
221
+ },
222
+ },
223
+ },
224
+ playerView: (state, playerId) => {
225
+ // Return filtered state for this player
226
+ return { ...state, value: playerId === "p1" ? state.value : 0 };
227
+ },
228
+ };
229
+
230
+ const result = validateGameDefinition(definition);
231
+ expect(result.success).toBe(true);
232
+ });
233
+
234
+ it("should work without playerView function", () => {
235
+ const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
236
+ name: "Test Game",
237
+ setup: () => ({ value: 0, phase: "start" }),
238
+ moves: {
239
+ increment: {
240
+ reducer: (draft) => {
241
+ draft.value += 1;
242
+ },
243
+ },
244
+ decrement: {
245
+ reducer: (draft) => {
246
+ draft.value -= 1;
247
+ },
248
+ },
249
+ },
250
+ };
251
+
252
+ const result = validateGameDefinition(definition);
253
+ expect(result.success).toBe(true);
254
+ });
255
+
256
+ it("should reject non-function playerView", () => {
257
+ const definition = {
258
+ name: "Test Game",
259
+ setup: () => ({ value: 0, phase: "start" }),
260
+ moves: {
261
+ increment: {
262
+ reducer: (draft: SimpleGameState) => {
263
+ draft.value += 1;
264
+ },
265
+ },
266
+ decrement: {
267
+ reducer: (draft: SimpleGameState) => {
268
+ draft.value -= 1;
269
+ },
270
+ },
271
+ },
272
+ playerView: "not a function",
273
+ };
274
+
275
+ const result = validateGameDefinition(definition as any);
276
+ expect(result.success).toBe(false);
277
+ if (!result.success) {
278
+ expect(result.error).toContain("playerView");
279
+ expect(result.error).toContain("function");
280
+ }
281
+ });
282
+ });
283
+
284
+ describe("name validation", () => {
285
+ it("should require a non-empty name", () => {
286
+ const definition = {
287
+ name: "",
288
+ setup: () => ({ value: 0, phase: "start" }),
289
+ moves: {
290
+ increment: {
291
+ reducer: (draft: SimpleGameState) => {
292
+ draft.value += 1;
293
+ },
294
+ },
295
+ decrement: {
296
+ reducer: (draft: SimpleGameState) => {
297
+ draft.value -= 1;
298
+ },
299
+ },
300
+ },
301
+ };
302
+
303
+ const result = validateGameDefinition(definition as any);
304
+ expect(result.success).toBe(false);
305
+ if (!result.success) {
306
+ expect(result.error).toContain("name");
307
+ }
308
+ });
309
+ });
310
+
311
+ // Note: minPlayers and maxPlayers are not part of GameDefinition spec
312
+ // Player count validation is handled at the application level, not in the core engine
313
+
314
+ describe("comprehensive validation (Task 10.13)", () => {
315
+ it("should validate all fields together", () => {
316
+ const validDefinition: GameDefinition<SimpleGameState, SimpleMoves> = {
317
+ name: "Complete Game",
318
+ setup: (players) => ({
319
+ value: players.length,
320
+ phase: "start",
321
+ }),
322
+ moves: {
323
+ increment: {
324
+ reducer: (draft) => {
325
+ draft.value += 1;
326
+ },
327
+ },
328
+ decrement: {
329
+ reducer: (draft) => {
330
+ draft.value -= 1;
331
+ },
332
+ },
333
+ },
334
+ endIf: (state) => {
335
+ if (state.value >= 10) {
336
+ return { winner: "p1", reason: "goal-reached" };
337
+ }
338
+ return undefined;
339
+ },
340
+ playerView: (state) => state,
341
+ };
342
+
343
+ const result = validateGameDefinition(validDefinition);
344
+ expect(result.success).toBe(true);
345
+ });
346
+
347
+ it("should provide detailed error messages for multiple errors", () => {
348
+ const invalidDefinition = {
349
+ name: "",
350
+ setup: "not a function",
351
+ moves: null,
352
+ };
353
+
354
+ const result = validateGameDefinition(invalidDefinition as any);
355
+ expect(result.success).toBe(false);
356
+ // Should report multiple validation failures
357
+ });
358
+ });
359
+ });
@@ -0,0 +1,291 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { Draft } from "immer";
3
+ import { createPlayerId } from "../../types";
4
+ import type { GameDefinition } from "../game-definition";
5
+ import type { GameMoveDefinitions } from "../move-definitions";
6
+
7
+ /**
8
+ * Test suite for GameDefinition type system
9
+ *
10
+ * Task 10.1: Write tests for GameDefinition type with generics
11
+ *
12
+ * Validates:
13
+ * - Generic type parameters work correctly
14
+ * - All required fields are present
15
+ * - Optional fields work as expected
16
+ * - Type safety is preserved
17
+ */
18
+
19
+ // Example game state for testing
20
+ type TestGameState = {
21
+ players: Array<{ id: string; name: string; score: number }>;
22
+ currentPlayerIndex: number;
23
+ phase: "setup" | "playing" | "ended";
24
+ winner?: string;
25
+ };
26
+
27
+ // Example moves for testing
28
+ type TestMoves = {
29
+ incrementScore: { playerId: string; amount: number };
30
+ nextPlayer: Record<string, never>;
31
+ endGame: { winnerId: string };
32
+ };
33
+
34
+ describe("GameDefinition - Type System", () => {
35
+ describe("basic structure", () => {
36
+ it("should create a valid GameDefinition with all required fields", () => {
37
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
38
+ incrementScore: {
39
+ reducer: (draft, context) => {
40
+ const player = draft.players.find((p) => p.id === context.playerId);
41
+ if (player && context.params?.amount) {
42
+ player.score += context.params.amount as number;
43
+ }
44
+ },
45
+ },
46
+ nextPlayer: {
47
+ reducer: (draft) => {
48
+ draft.currentPlayerIndex =
49
+ (draft.currentPlayerIndex + 1) % draft.players.length;
50
+ },
51
+ },
52
+ endGame: {
53
+ reducer: (draft, context) => {
54
+ draft.phase = "ended";
55
+ if (context.params?.winnerId) {
56
+ draft.winner = context.params.winnerId as string;
57
+ }
58
+ },
59
+ },
60
+ };
61
+
62
+ const definition: GameDefinition<TestGameState, TestMoves> = {
63
+ name: "Test Game",
64
+ setup: (players) => ({
65
+ players: players.map((p, i) => ({
66
+ id: p.id,
67
+ name: p.name || `Player ${i + 1}`,
68
+ score: 0,
69
+ })),
70
+ currentPlayerIndex: 0,
71
+ phase: "setup",
72
+ }),
73
+ moves,
74
+ };
75
+
76
+ expect(definition.name).toBe("Test Game");
77
+ expect(definition.setup).toBeFunction();
78
+ expect(definition.moves).toEqual(moves);
79
+ });
80
+
81
+ it("should work with optional endIf field", () => {
82
+ const definition: GameDefinition<TestGameState, TestMoves> = {
83
+ name: "Test Game",
84
+ setup: () => ({
85
+ players: [],
86
+ currentPlayerIndex: 0,
87
+ phase: "setup",
88
+ }),
89
+ moves: {} as GameMoveDefinitions<TestGameState, TestMoves>,
90
+ endIf: (state) => {
91
+ if (state.phase === "ended" && state.winner) {
92
+ return {
93
+ winner: state.winner,
94
+ reason: "game-ended",
95
+ };
96
+ }
97
+ return undefined;
98
+ },
99
+ };
100
+
101
+ expect(definition.endIf).toBeFunction();
102
+ });
103
+
104
+ it("should work with optional playerView field", () => {
105
+ const definition: GameDefinition<TestGameState, TestMoves> = {
106
+ name: "Test Game",
107
+ setup: () => ({
108
+ players: [],
109
+ currentPlayerIndex: 0,
110
+ phase: "setup",
111
+ }),
112
+ moves: {} as GameMoveDefinitions<TestGameState, TestMoves>,
113
+ playerView: (state, playerId) => {
114
+ // Filter state for this player
115
+ return {
116
+ ...state,
117
+ players: state.players.map((p) => ({
118
+ ...p,
119
+ // Hide scores from other players
120
+ score: p.id === playerId ? p.score : 0,
121
+ })),
122
+ };
123
+ },
124
+ };
125
+
126
+ expect(definition.playerView).toBeFunction();
127
+ });
128
+ });
129
+
130
+ describe("generic type safety", () => {
131
+ it("should enforce state type in setup function", () => {
132
+ const definition: GameDefinition<TestGameState, TestMoves> = {
133
+ name: "Test Game",
134
+ setup: (players) => {
135
+ // TypeScript should enforce return type
136
+ const state: TestGameState = {
137
+ players: players.map((p) => ({
138
+ id: p.id,
139
+ name: p.name || "Player",
140
+ score: 0,
141
+ })),
142
+ currentPlayerIndex: 0,
143
+ phase: "setup",
144
+ };
145
+ return state;
146
+ },
147
+ moves: {} as GameMoveDefinitions<TestGameState, TestMoves>,
148
+ };
149
+
150
+ const players = [
151
+ { id: createPlayerId("p1"), name: "Alice" },
152
+ { id: createPlayerId("p2"), name: "Bob" },
153
+ ];
154
+ const initialState = definition.setup(players);
155
+
156
+ expect(initialState.players).toHaveLength(2);
157
+ expect(initialState.phase).toBe("setup");
158
+ });
159
+
160
+ it("should enforce move types in MoveDefinitions", () => {
161
+ // This test validates type safety at compile time
162
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
163
+ incrementScore: {
164
+ reducer: (draft: Draft<TestGameState>, context) => {
165
+ // context.params should be type-checked
166
+ const amount = context.params?.amount as number;
167
+ const player = draft.players[0];
168
+ if (player) {
169
+ player.score += amount;
170
+ }
171
+ },
172
+ },
173
+ nextPlayer: {
174
+ reducer: (draft: Draft<TestGameState>) => {
175
+ draft.currentPlayerIndex += 1;
176
+ },
177
+ },
178
+ endGame: {
179
+ reducer: (draft: Draft<TestGameState>, context) => {
180
+ draft.phase = "ended";
181
+ draft.winner = context.params?.winnerId as string;
182
+ },
183
+ },
184
+ };
185
+
186
+ expect(Object.keys(moves)).toEqual([
187
+ "incrementScore",
188
+ "nextPlayer",
189
+ "endGame",
190
+ ]);
191
+ });
192
+
193
+ it("should enforce state type in endIf function", () => {
194
+ const definition: GameDefinition<TestGameState, TestMoves> = {
195
+ name: "Test Game",
196
+ setup: () => ({
197
+ players: [],
198
+ currentPlayerIndex: 0,
199
+ phase: "setup",
200
+ }),
201
+ moves: {} as GameMoveDefinitions<TestGameState, TestMoves>,
202
+ endIf: (state: TestGameState) => {
203
+ // Should have access to all state fields
204
+ if (state.phase === "ended") {
205
+ return {
206
+ winner: state.winner || "none",
207
+ reason: "phase-ended",
208
+ };
209
+ }
210
+ return undefined;
211
+ },
212
+ };
213
+
214
+ const testState: TestGameState = {
215
+ players: [],
216
+ currentPlayerIndex: 0,
217
+ phase: "ended",
218
+ winner: "p1",
219
+ };
220
+
221
+ const result = definition.endIf?.(testState);
222
+ expect(result).toEqual({ winner: "p1", reason: "phase-ended" });
223
+ });
224
+
225
+ it("should enforce state type in playerView function", () => {
226
+ const definition: GameDefinition<TestGameState, TestMoves> = {
227
+ name: "Test Game",
228
+ setup: () => ({
229
+ players: [],
230
+ currentPlayerIndex: 0,
231
+ phase: "setup",
232
+ }),
233
+ moves: {} as GameMoveDefinitions<TestGameState, TestMoves>,
234
+ playerView: (state: TestGameState, playerId: string) => {
235
+ // Should have access to all state fields
236
+ return {
237
+ ...state,
238
+ players: state.players.map((p) => ({
239
+ ...p,
240
+ score: p.id === playerId ? p.score : 0,
241
+ })),
242
+ };
243
+ },
244
+ };
245
+
246
+ const testState: TestGameState = {
247
+ players: [
248
+ { id: "p1", name: "Alice", score: 10 },
249
+ { id: "p2", name: "Bob", score: 20 },
250
+ ],
251
+ currentPlayerIndex: 0,
252
+ phase: "playing",
253
+ };
254
+
255
+ const filteredState = definition.playerView?.(testState, "p1");
256
+ expect(filteredState?.players[0]?.score).toBe(10);
257
+ expect(filteredState?.players[1]?.score).toBe(0);
258
+ });
259
+ });
260
+
261
+ describe("setup function", () => {
262
+ it("should be deterministic (same players -> same state)", () => {
263
+ const definition: GameDefinition<TestGameState, TestMoves> = {
264
+ name: "Test Game",
265
+ setup: (players) => ({
266
+ players: players.map((p, i) => ({
267
+ id: p.id,
268
+ name: p.name || `Player ${i + 1}`,
269
+ score: 0,
270
+ })),
271
+ currentPlayerIndex: 0,
272
+ phase: "setup",
273
+ }),
274
+ moves: {} as GameMoveDefinitions<TestGameState, TestMoves>,
275
+ };
276
+
277
+ const players = [
278
+ { id: createPlayerId("p1"), name: "Alice" },
279
+ { id: createPlayerId("p2"), name: "Bob" },
280
+ ];
281
+
282
+ const state1 = definition.setup(players);
283
+ const state2 = definition.setup(players);
284
+
285
+ expect(state1).toEqual(state2);
286
+ });
287
+ });
288
+
289
+ // Note: minPlayers and maxPlayers are not part of GameDefinition spec
290
+ // Player count validation is handled at the application level, not in the core engine
291
+ });