@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,431 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { Draft } from "immer";
3
+ import type { GameMoveDefinition } from "../game-definition/move-definitions";
4
+ import { createMockContext } from "../testing/test-context-factory";
5
+ import type { PlayerId } from "../types";
6
+ import { createPlayerId } from "../types";
7
+ import {
8
+ canExecuteMove,
9
+ executeMove,
10
+ getMove,
11
+ getMoveIds,
12
+ moveExists,
13
+ } from "./move-executor";
14
+ import type { MoveContext } from "./move-system";
15
+
16
+ describe("Move Executor", () => {
17
+ type TestGameState = {
18
+ players: Record<PlayerId, { life: number; mana: number }>;
19
+ turnCount: number;
20
+ };
21
+
22
+ const player1 = createPlayerId("p1");
23
+ const player2 = createPlayerId("p2");
24
+
25
+ const initialState: TestGameState = {
26
+ players: {
27
+ [player1]: { life: 20, mana: 5 },
28
+ [player2]: { life: 20, mana: 5 },
29
+ },
30
+ turnCount: 1,
31
+ };
32
+
33
+ const testMoves: Record<string, GameMoveDefinition<TestGameState>> = {
34
+ "spend-mana": {
35
+ condition: (state: TestGameState, context: MoveContext) =>
36
+ state.players[context.playerId].mana >= 2,
37
+ reducer: (draft: Draft<TestGameState>, context: MoveContext) => {
38
+ draft.players[context.playerId].mana -= 2;
39
+ },
40
+ },
41
+ "deal-damage": {
42
+ condition: (state: TestGameState, context: MoveContext) => {
43
+ if (!context.targets?.[0]) return false;
44
+ const targetId = context.targets[0][0] as PlayerId;
45
+ return targetId in state.players;
46
+ },
47
+ reducer: (draft: Draft<TestGameState>, context: MoveContext) => {
48
+ const targetId = context.targets?.[0]?.[0] as PlayerId;
49
+ if (targetId) {
50
+ draft.players[targetId].life -= 3;
51
+ }
52
+ },
53
+ },
54
+ "next-turn": {
55
+ reducer: (draft: Draft<TestGameState>) => {
56
+ draft.turnCount += 1;
57
+ },
58
+ },
59
+ };
60
+
61
+ describe("executeMove", () => {
62
+ it("should execute valid move successfully", () => {
63
+ const context: MoveContext = createMockContext({
64
+ playerId: player1,
65
+ params: {},
66
+ });
67
+ const result = executeMove(
68
+ initialState,
69
+ "spend-mana",
70
+ context,
71
+ testMoves,
72
+ );
73
+
74
+ expect(result.success).toBe(true);
75
+ if (result.success) {
76
+ expect(result.state.players[player1].mana).toBe(3);
77
+ expect(result.state.players[player2].mana).toBe(5); // Unchanged
78
+ }
79
+ });
80
+
81
+ it("should reject move with failed condition", () => {
82
+ const lowManaState: TestGameState = {
83
+ ...initialState,
84
+ players: {
85
+ ...initialState.players,
86
+ [player1]: { life: 20, mana: 1 },
87
+ },
88
+ };
89
+
90
+ const context: MoveContext = createMockContext({
91
+ playerId: player1,
92
+ params: {},
93
+ });
94
+ const result = executeMove(
95
+ lowManaState,
96
+ "spend-mana",
97
+ context,
98
+ testMoves,
99
+ );
100
+
101
+ expect(result.success).toBe(false);
102
+ if (!result.success) {
103
+ expect(result.error).toContain("condition not met");
104
+ expect(result.errorCode).toBe("CONDITION_FAILED");
105
+ }
106
+ });
107
+
108
+ it("should reject non-existent move", () => {
109
+ const context: MoveContext = createMockContext({
110
+ playerId: player1,
111
+ params: {},
112
+ });
113
+ const result = executeMove(
114
+ initialState,
115
+ "nonexistent",
116
+ context,
117
+ testMoves,
118
+ );
119
+
120
+ expect(result.success).toBe(false);
121
+ if (!result.success) {
122
+ expect(result.error).toContain("does not exist");
123
+ expect(result.errorCode).toBe("MOVE_NOT_FOUND");
124
+ }
125
+ });
126
+
127
+ it("should execute move without condition", () => {
128
+ const context: MoveContext = createMockContext({
129
+ playerId: player1,
130
+ params: {},
131
+ });
132
+ const result = executeMove(initialState, "next-turn", context, testMoves);
133
+
134
+ expect(result.success).toBe(true);
135
+ if (result.success) {
136
+ expect(result.state.turnCount).toBe(2);
137
+ }
138
+ });
139
+
140
+ it("should execute move with targets", () => {
141
+ const context: MoveContext = createMockContext({
142
+ playerId: player1,
143
+ params: {},
144
+ targets: [[player2]],
145
+ });
146
+ const result = executeMove(
147
+ initialState,
148
+ "deal-damage",
149
+ context,
150
+ testMoves,
151
+ );
152
+
153
+ expect(result.success).toBe(true);
154
+ if (result.success) {
155
+ expect(result.state.players[player2].life).toBe(17);
156
+ }
157
+ });
158
+
159
+ it("should handle condition errors gracefully", () => {
160
+ const brokenMove: GameMoveDefinition<TestGameState> = {
161
+ condition: () => {
162
+ throw new Error("Condition error");
163
+ },
164
+ reducer: (draft: Draft<TestGameState>) => draft,
165
+ };
166
+
167
+ const moves = { ...testMoves, broken: brokenMove };
168
+ const context: MoveContext = createMockContext({
169
+ playerId: player1,
170
+ params: {},
171
+ });
172
+ const result = executeMove(initialState, "broken", context, moves);
173
+
174
+ expect(result.success).toBe(false);
175
+ if (!result.success) {
176
+ expect(result.errorCode).toBe("CONDITION_ERROR");
177
+ expect(result.error).toContain("Error checking condition");
178
+ }
179
+ });
180
+
181
+ it("should handle reducer errors gracefully", () => {
182
+ const brokenMove: GameMoveDefinition<TestGameState> = {
183
+ reducer: () => {
184
+ throw new Error("Reducer error");
185
+ },
186
+ };
187
+
188
+ const moves = { ...testMoves, "broken-reducer": brokenMove };
189
+ const context: MoveContext = createMockContext({
190
+ playerId: player1,
191
+ params: {},
192
+ });
193
+ const result = executeMove(
194
+ initialState,
195
+ "broken-reducer",
196
+ context,
197
+ moves,
198
+ );
199
+
200
+ expect(result.success).toBe(false);
201
+ if (!result.success) {
202
+ expect(result.errorCode).toBe("EXECUTION_ERROR");
203
+ expect(result.error).toContain("Error executing move");
204
+ }
205
+ });
206
+
207
+ it("should not mutate original state", () => {
208
+ const context: MoveContext = createMockContext({
209
+ playerId: player1,
210
+ params: {},
211
+ });
212
+ executeMove(initialState, "spend-mana", context, testMoves);
213
+
214
+ // Original state should be unchanged
215
+ expect(initialState.players[player1].mana).toBe(5);
216
+ });
217
+ });
218
+
219
+ describe("canExecuteMove", () => {
220
+ it("should return true for valid move", () => {
221
+ const context: MoveContext = createMockContext({
222
+ playerId: player1,
223
+ params: {},
224
+ });
225
+ const canExecute = canExecuteMove(
226
+ initialState,
227
+ "spend-mana",
228
+ context,
229
+ testMoves,
230
+ );
231
+
232
+ expect(canExecute).toBe(true);
233
+ });
234
+
235
+ it("should return false for invalid move", () => {
236
+ const lowManaState: TestGameState = {
237
+ ...initialState,
238
+ players: {
239
+ ...initialState.players,
240
+ [player1]: { life: 20, mana: 1 },
241
+ },
242
+ };
243
+
244
+ const context: MoveContext = createMockContext({
245
+ playerId: player1,
246
+ params: {},
247
+ });
248
+ const canExecute = canExecuteMove(
249
+ lowManaState,
250
+ "spend-mana",
251
+ context,
252
+ testMoves,
253
+ );
254
+
255
+ expect(canExecute).toBe(false);
256
+ });
257
+
258
+ it("should return false for non-existent move", () => {
259
+ const context: MoveContext = createMockContext({
260
+ playerId: player1,
261
+ params: {},
262
+ });
263
+ const canExecute = canExecuteMove(
264
+ initialState,
265
+ "nonexistent",
266
+ context,
267
+ testMoves,
268
+ );
269
+
270
+ expect(canExecute).toBe(false);
271
+ });
272
+
273
+ it("should return true for move without condition", () => {
274
+ const context: MoveContext = createMockContext({
275
+ playerId: player1,
276
+ params: {},
277
+ });
278
+ const canExecute = canExecuteMove(
279
+ initialState,
280
+ "next-turn",
281
+ context,
282
+ testMoves,
283
+ );
284
+
285
+ expect(canExecute).toBe(true);
286
+ });
287
+
288
+ it("should not execute the move (dry run)", () => {
289
+ const context: MoveContext = createMockContext({
290
+ playerId: player1,
291
+ params: {},
292
+ });
293
+ canExecuteMove(initialState, "spend-mana", context, testMoves);
294
+
295
+ // State should be unchanged
296
+ expect(initialState.players[player1].mana).toBe(5);
297
+ });
298
+
299
+ it("should handle condition errors by returning false", () => {
300
+ const brokenMove: GameMoveDefinition<TestGameState> = {
301
+ condition: () => {
302
+ throw new Error("Condition error");
303
+ },
304
+ reducer: (draft: Draft<TestGameState>) => draft,
305
+ };
306
+
307
+ const moves = { ...testMoves, broken: brokenMove };
308
+ const context: MoveContext = createMockContext({
309
+ playerId: player1,
310
+ params: {},
311
+ });
312
+ const canExecute = canExecuteMove(initialState, "broken", context, moves);
313
+
314
+ expect(canExecute).toBe(false);
315
+ });
316
+ });
317
+
318
+ describe("getMove", () => {
319
+ it("should return move definition if exists", () => {
320
+ const move = getMove("spend-mana", testMoves);
321
+
322
+ expect(move).toBeDefined();
323
+ expect(move?.reducer).toBeDefined();
324
+ });
325
+
326
+ it("should return undefined if move does not exist", () => {
327
+ const move = getMove("nonexistent", testMoves);
328
+
329
+ expect(move).toBeUndefined();
330
+ });
331
+ });
332
+
333
+ describe("getMoveIds", () => {
334
+ it("should return all move IDs", () => {
335
+ const ids = getMoveIds(testMoves);
336
+
337
+ expect(ids).toContain("spend-mana");
338
+ expect(ids).toContain("deal-damage");
339
+ expect(ids).toContain("next-turn");
340
+ expect(ids).toHaveLength(3);
341
+ });
342
+
343
+ it("should return empty array for empty moves", () => {
344
+ const ids = getMoveIds({});
345
+
346
+ expect(ids).toEqual([]);
347
+ });
348
+ });
349
+
350
+ describe("moveExists", () => {
351
+ it("should return true if move exists", () => {
352
+ expect(moveExists("spend-mana", testMoves)).toBe(true);
353
+ expect(moveExists("deal-damage", testMoves)).toBe(true);
354
+ });
355
+
356
+ it("should return false if move does not exist", () => {
357
+ expect(moveExists("nonexistent", testMoves)).toBe(false);
358
+ });
359
+ });
360
+
361
+ describe("Integration: Validation Pipeline", () => {
362
+ it("should follow full validation pipeline", () => {
363
+ const context: MoveContext = createMockContext({
364
+ playerId: player1,
365
+ params: {},
366
+ });
367
+
368
+ // 1. Check if move can be executed
369
+ const canExecute = canExecuteMove(
370
+ initialState,
371
+ "spend-mana",
372
+ context,
373
+ testMoves,
374
+ );
375
+ expect(canExecute).toBe(true);
376
+
377
+ // 2. Execute the move
378
+ const result = executeMove(
379
+ initialState,
380
+ "spend-mana",
381
+ context,
382
+ testMoves,
383
+ );
384
+ expect(result.success).toBe(true);
385
+
386
+ // 3. Use new state
387
+ if (result.success) {
388
+ const newState = result.state;
389
+ expect(newState.players[player1].mana).toBe(3);
390
+
391
+ // 4. Check if move can still be executed
392
+ const canExecuteAgain = canExecuteMove(
393
+ newState,
394
+ "spend-mana",
395
+ context,
396
+ testMoves,
397
+ );
398
+ expect(canExecuteAgain).toBe(true); // Still >= 2 mana
399
+ }
400
+ });
401
+
402
+ it("should prevent invalid moves from being executed", () => {
403
+ const lowManaState: TestGameState = {
404
+ ...initialState,
405
+ players: {
406
+ ...initialState.players,
407
+ [player1]: { life: 20, mana: 1 },
408
+ },
409
+ };
410
+
411
+ const context: MoveContext = createMockContext({
412
+ playerId: player1,
413
+ params: {},
414
+ });
415
+
416
+ // Pre-check prevents unnecessary execution
417
+ if (canExecuteMove(lowManaState, "spend-mana", context, testMoves)) {
418
+ const result = executeMove(
419
+ lowManaState,
420
+ "spend-mana",
421
+ context,
422
+ testMoves,
423
+ );
424
+ expect(result.success).toBe(true);
425
+ } else {
426
+ // Move was correctly prevented
427
+ expect(true).toBe(true);
428
+ }
429
+ });
430
+ });
431
+ });
@@ -0,0 +1,195 @@
1
+ import { produce } from "immer";
2
+ import type { GameMoveDefinition } from "../game-definition/move-definitions";
3
+ import type { MoveContext, MoveResult } from "./move-system";
4
+
5
+ /**
6
+ * Generic move map type for runtime lookup
7
+ *
8
+ * Used by executor functions that need to look up moves by string ID.
9
+ * For type-safe move definitions in game definitions, use GameMoveDefinitions instead.
10
+ */
11
+ type GenericMoveMap<TGameState> = Record<
12
+ string,
13
+ GameMoveDefinition<TGameState>
14
+ >;
15
+
16
+ /**
17
+ * Execute a move with full validation pipeline
18
+ *
19
+ * Pipeline:
20
+ * 1. Validate move exists
21
+ * 2. Check condition (if present)
22
+ * 3. Execute reducer with Immer
23
+ * 4. Return result (success or failure)
24
+ *
25
+ * @param state - Current game state
26
+ * @param moveId - ID of move to execute
27
+ * @param context - Move context
28
+ * @param moves - Available moves
29
+ * @returns MoveResult with new state or error
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const result = executeMove(
34
+ * gameState,
35
+ * 'draw-card',
36
+ * { playerId: 'p1' },
37
+ * gameMoves
38
+ * );
39
+ *
40
+ * if (result.success) {
41
+ * gameState = result.state;
42
+ * } else {
43
+ * console.error(result.error);
44
+ * }
45
+ * ```
46
+ */
47
+ export function executeMove<TGameState>(
48
+ state: TGameState,
49
+ moveId: string,
50
+ context: MoveContext,
51
+ moves: GenericMoveMap<TGameState>,
52
+ ): MoveResult<TGameState> {
53
+ // 1. Validate move exists
54
+ const moveDef = moves[moveId];
55
+ if (!moveDef) {
56
+ return {
57
+ success: false,
58
+ error: `Move '${moveId}' does not exist`,
59
+ errorCode: "MOVE_NOT_FOUND",
60
+ errorContext: { moveId },
61
+ };
62
+ }
63
+
64
+ // 2. Check condition
65
+ if (moveDef.condition) {
66
+ try {
67
+ const isValid = moveDef.condition(state, context);
68
+ if (!isValid) {
69
+ return {
70
+ success: false,
71
+ error: `Move '${moveId}' condition not met`,
72
+ errorCode: "CONDITION_FAILED",
73
+ errorContext: { moveId },
74
+ };
75
+ }
76
+ } catch (error) {
77
+ return {
78
+ success: false,
79
+ error: `Error checking condition for move '${moveId}': ${error instanceof Error ? error.message : String(error)}`,
80
+ errorCode: "CONDITION_ERROR",
81
+ errorContext: {
82
+ moveId,
83
+ originalError: error instanceof Error ? error.message : String(error),
84
+ },
85
+ };
86
+ }
87
+ }
88
+
89
+ // 3. Execute reducer
90
+ try {
91
+ const nextState = produce(state, (draft) => {
92
+ moveDef.reducer(draft, context);
93
+ });
94
+
95
+ return {
96
+ success: true,
97
+ state: nextState,
98
+ };
99
+ } catch (error) {
100
+ return {
101
+ success: false,
102
+ error: `Error executing move '${moveId}': ${error instanceof Error ? error.message : String(error)}`,
103
+ errorCode: "EXECUTION_ERROR",
104
+ errorContext: {
105
+ moveId,
106
+ originalError: error instanceof Error ? error.message : String(error),
107
+ },
108
+ };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Check if a move can be executed without actually executing it
114
+ *
115
+ * Validates:
116
+ * 1. Move exists
117
+ * 2. Condition passes (if present)
118
+ *
119
+ * Does NOT execute the reducer or modify state.
120
+ *
121
+ * @param state - Current game state
122
+ * @param moveId - ID of move to check
123
+ * @param context - Move context
124
+ * @param moves - Available moves
125
+ * @returns True if move can be executed
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * if (canExecuteMove(gameState, 'play-card', context, moves)) {
130
+ * const result = executeMove(gameState, 'play-card', context, moves);
131
+ * }
132
+ * ```
133
+ */
134
+ export function canExecuteMove<TGameState>(
135
+ state: TGameState,
136
+ moveId: string,
137
+ context: MoveContext,
138
+ moves: GenericMoveMap<TGameState>,
139
+ ): boolean {
140
+ const moveDef = moves[moveId];
141
+ if (!moveDef) {
142
+ return false;
143
+ }
144
+
145
+ if (!moveDef.condition) {
146
+ return true; // No condition means always valid
147
+ }
148
+
149
+ try {
150
+ const result = moveDef.condition(state, context);
151
+ return result === true; // Support both boolean and ConditionFailure returns
152
+ } catch {
153
+ return false; // Condition error = invalid
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Get move definition by ID
159
+ *
160
+ * @param moveId - ID of move to retrieve
161
+ * @param moves - Available moves
162
+ * @returns Move definition or undefined
163
+ */
164
+ export function getMove<TGameState>(
165
+ moveId: string,
166
+ moves: GenericMoveMap<TGameState>,
167
+ ): GameMoveDefinition<TGameState> | undefined {
168
+ return moves[moveId];
169
+ }
170
+
171
+ /**
172
+ * Get all move IDs
173
+ *
174
+ * @param moves - Available moves
175
+ * @returns Array of move IDs
176
+ */
177
+ export function getMoveIds<TGameState>(
178
+ moves: GenericMoveMap<TGameState>,
179
+ ): string[] {
180
+ return Object.keys(moves);
181
+ }
182
+
183
+ /**
184
+ * Check if a move exists
185
+ *
186
+ * @param moveId - ID of move to check
187
+ * @param moves - Available moves
188
+ * @returns True if move exists
189
+ */
190
+ export function moveExists<TGameState>(
191
+ moveId: string,
192
+ moves: GenericMoveMap<TGameState>,
193
+ ): boolean {
194
+ return moveId in moves;
195
+ }