@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,187 @@
1
+ /**
2
+ * Test Context Factory
3
+ *
4
+ * Utilities for creating MoveContext objects in tests.
5
+ * Provides mock implementations of engine services for unit testing.
6
+ */
7
+
8
+ import type { HistoryOperations } from "../history/history-operations";
9
+ import type { MoveContext, MoveContextInput } from "../moves/move-system";
10
+ import type { CardOperations } from "../operations/card-operations";
11
+ import type { CounterOperations } from "../operations/counter-operations";
12
+ import type { GameOperations } from "../operations/game-operations";
13
+ import type { ZoneOperations } from "../operations/zone-operations";
14
+ import { SeededRNG } from "../rng/seeded-rng";
15
+ import type { CardId, PlayerId, ZoneId } from "../types";
16
+
17
+ /**
18
+ * Create a mock MoveContext for testing
19
+ *
20
+ * Builds a full MoveContext with mock implementations of engine services.
21
+ * Useful for unit testing reducers and conditions without a full engine.
22
+ *
23
+ * @param input - Partial context input
24
+ * @param options - Optional mock implementations
25
+ * @returns Full MoveContext with mocks
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * const context = createMockContext({
30
+ * playerId: 'p1',
31
+ * params: { cardId: 'card-123' }
32
+ * });
33
+ *
34
+ * // Use in reducer test
35
+ * const reducer: MoveReducer<GameState> = (draft, ctx) => {
36
+ * // ctx has full MoveContext type
37
+ * };
38
+ * reducer(draft, context);
39
+ * ```
40
+ */
41
+ export function createMockContext<TParams = any>(
42
+ input: MoveContextInput<TParams>,
43
+ options?: {
44
+ rng?: SeededRNG;
45
+ zones?: Partial<ZoneOperations>;
46
+ cards?: Partial<CardOperations<any>>;
47
+ game?: Partial<GameOperations>;
48
+ counters?: Partial<CounterOperations>;
49
+ registry?: any;
50
+ flow?: {
51
+ currentPhase?: string;
52
+ currentSegment?: string;
53
+ turn: number;
54
+ currentPlayer: PlayerId;
55
+ isFirstTurn: boolean;
56
+ endPhase?: () => void;
57
+ endSegment?: () => void;
58
+ endTurn?: () => void;
59
+ };
60
+ endGame?: (result: {
61
+ winner?: PlayerId;
62
+ reason: string;
63
+ metadata?: Record<string, unknown>;
64
+ }) => void;
65
+ trackers?: {
66
+ check(name: string, playerId?: PlayerId): boolean;
67
+ mark(name: string, playerId?: PlayerId): void;
68
+ unmark(name: string, playerId?: PlayerId): void;
69
+ };
70
+ history?: Partial<HistoryOperations>;
71
+ },
72
+ ): MoveContext<TParams> {
73
+ const mockZones: ZoneOperations = {
74
+ moveCard: () => {},
75
+ getCardsInZone: () => [],
76
+ shuffleZone: () => {},
77
+ getCardZone: () => undefined,
78
+ drawCards: () => [],
79
+ mulligan: () => {},
80
+ bulkMove: () => [],
81
+ createDeck: () => [],
82
+ ...options?.zones,
83
+ };
84
+
85
+ const mockCards: CardOperations<any> = {
86
+ getCardMeta: () => ({}),
87
+ updateCardMeta: () => {},
88
+ setCardMeta: () => {},
89
+ getCardOwner: () => undefined as any,
90
+ queryCards: () => [],
91
+ ...options?.cards,
92
+ };
93
+
94
+ const mockGame: GameOperations = {
95
+ setOTP: () => {},
96
+ getOTP: () => undefined,
97
+ setChoosingFirstPlayer: () => {},
98
+ getChoosingFirstPlayer: () => undefined,
99
+ setPendingMulligan: () => {},
100
+ getPendingMulligan: () => [],
101
+ addPendingMulligan: () => {},
102
+ removePendingMulligan: () => {},
103
+ ...options?.game,
104
+ };
105
+
106
+ const mockCounters: CounterOperations = {
107
+ setFlag: () => {},
108
+ getFlag: () => false,
109
+ addCounter: () => {},
110
+ removeCounter: () => {},
111
+ getCounter: () => 0,
112
+ clearCounter: () => {},
113
+ clearAllCounters: () => {},
114
+ getCardsWithFlag: () => [],
115
+ getCardsWithCounter: () => [],
116
+ ...options?.counters,
117
+ };
118
+
119
+ return {
120
+ ...input,
121
+ rng: options?.rng || new SeededRNG("test-seed"),
122
+ zones: mockZones,
123
+ cards: mockCards,
124
+ game: mockGame,
125
+ counters: mockCounters,
126
+ registry: options?.registry,
127
+ flow: options?.flow
128
+ ? {
129
+ ...options.flow,
130
+ endPhase: options.flow.endPhase || (() => {}),
131
+ endSegment: options.flow.endSegment || (() => {}),
132
+ endTurn: options.flow.endTurn || (() => {}),
133
+ }
134
+ : undefined,
135
+ endGame: options?.endGame || (() => {}),
136
+ trackers: options?.trackers || {
137
+ check: () => false,
138
+ mark: () => {},
139
+ unmark: () => {},
140
+ },
141
+ history: {
142
+ log: () => {},
143
+ ...options?.history,
144
+ },
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Create a mock CardOperations for testing
150
+ *
151
+ * @param overrides - Optional method overrides
152
+ * @returns Mock CardOperations
153
+ */
154
+ export function createMockCardOperations<TCardMeta = any>(
155
+ overrides?: Partial<CardOperations<TCardMeta>>,
156
+ ): CardOperations<TCardMeta> {
157
+ return {
158
+ getCardMeta: () => ({}) as TCardMeta,
159
+ updateCardMeta: () => {},
160
+ setCardMeta: () => {},
161
+ getCardOwner: () => undefined as any,
162
+ queryCards: () => [],
163
+ ...overrides,
164
+ };
165
+ }
166
+
167
+ /**
168
+ * Create a mock ZoneOperations for testing
169
+ *
170
+ * @param overrides - Optional method overrides
171
+ * @returns Mock ZoneOperations
172
+ */
173
+ export function createMockZoneOperations(
174
+ overrides?: Partial<ZoneOperations>,
175
+ ): ZoneOperations {
176
+ return {
177
+ moveCard: () => {},
178
+ getCardsInZone: () => [],
179
+ shuffleZone: () => {},
180
+ getCardZone: () => undefined,
181
+ drawCards: () => [],
182
+ mulligan: () => {},
183
+ bulkMove: () => [],
184
+ createDeck: () => [],
185
+ ...overrides,
186
+ };
187
+ }
@@ -0,0 +1,262 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { RuleEngine } from "../engine/rule-engine";
3
+ import type { GameDefinition } from "../game-definition/game-definition";
4
+ import type { GameMoveDefinitions } from "../game-definition/move-definitions";
5
+ import { createPlayerId, type PlayerId } from "../types";
6
+ import { expectGameEnd, expectGameNotEnded } from "./test-end-assertions";
7
+
8
+ /**
9
+ * Test state for end assertions
10
+ */
11
+ type EndTestState = {
12
+ players: Array<{
13
+ id: PlayerId;
14
+ name: string;
15
+ health: number;
16
+ }>;
17
+ turnNumber: number;
18
+ winner?: PlayerId;
19
+ };
20
+
21
+ type EndTestMoves = {
22
+ damagePlayer: { targetId: PlayerId; amount: number };
23
+ setWinner: { winnerId: PlayerId };
24
+ };
25
+
26
+ describe("test-end-assertions", () => {
27
+ function createTestEngine() {
28
+ const moves: GameMoveDefinitions<EndTestState, EndTestMoves> = {
29
+ damagePlayer: {
30
+ reducer: (draft, context) => {
31
+ if (context.params?.targetId && context.params?.amount) {
32
+ const target = draft.players.find(
33
+ (p) => p.id === context.params?.targetId,
34
+ );
35
+ if (target) {
36
+ target.health -= context.params.amount as number;
37
+ if (target.health <= 0) {
38
+ // Set winner to the other player
39
+ const winner = draft.players.find(
40
+ (p) => p.id !== context.params?.targetId,
41
+ );
42
+ if (winner) {
43
+ draft.winner = winner.id;
44
+ }
45
+ }
46
+ }
47
+ }
48
+ },
49
+ },
50
+ setWinner: {
51
+ reducer: (draft, context) => {
52
+ if (context.params?.winnerId) {
53
+ draft.winner = context.params.winnerId as PlayerId;
54
+ }
55
+ },
56
+ },
57
+ };
58
+
59
+ const gameDefinition: GameDefinition<EndTestState, EndTestMoves> = {
60
+ name: "End Test Game",
61
+ setup: (players) => ({
62
+ players: players.map((p) => ({
63
+ id: p.id as PlayerId,
64
+ name: p.name || "Player",
65
+ health: 10,
66
+ })),
67
+ turnNumber: 1,
68
+ }),
69
+ moves,
70
+ endIf: (state) => {
71
+ if (state.winner) {
72
+ return {
73
+ winner: state.winner,
74
+ reason: "Player eliminated",
75
+ };
76
+ }
77
+ return undefined;
78
+ },
79
+ };
80
+
81
+ const players = [
82
+ { id: createPlayerId("p1"), name: "Alice" },
83
+ { id: createPlayerId("p2"), name: "Bob" },
84
+ ];
85
+
86
+ return new RuleEngine(gameDefinition, players);
87
+ }
88
+
89
+ describe("expectGameEnd", () => {
90
+ it("should pass when game ends with expected winner", () => {
91
+ const engine = createTestEngine();
92
+
93
+ // Set winner
94
+ engine.executeMove("setWinner", {
95
+ playerId: createPlayerId("p1"),
96
+ params: { winnerId: createPlayerId("p1") },
97
+ });
98
+
99
+ // Should not throw
100
+ expectGameEnd(engine, createPlayerId("p1"));
101
+ });
102
+
103
+ it("should throw when game has not ended", () => {
104
+ const engine = createTestEngine();
105
+
106
+ expect(() => {
107
+ expectGameEnd(engine, createPlayerId("p1"));
108
+ }).toThrow(/Expected game to have ended/);
109
+ });
110
+
111
+ it("should throw when winner does not match", () => {
112
+ const engine = createTestEngine();
113
+
114
+ engine.executeMove("setWinner", {
115
+ playerId: createPlayerId("p1"),
116
+ params: { winnerId: createPlayerId("p1") },
117
+ });
118
+
119
+ expect(() => {
120
+ expectGameEnd(engine, createPlayerId("p2"));
121
+ }).toThrow(/Expected winner to be 'p2'/);
122
+ });
123
+
124
+ it("should work without specifying winner", () => {
125
+ const engine = createTestEngine();
126
+
127
+ engine.executeMove("setWinner", {
128
+ playerId: createPlayerId("p1"),
129
+ params: { winnerId: createPlayerId("p1") },
130
+ });
131
+
132
+ // Should not throw - just checks that game ended
133
+ expectGameEnd(engine);
134
+ });
135
+
136
+ it("should optionally check reason", () => {
137
+ const engine = createTestEngine();
138
+
139
+ engine.executeMove("setWinner", {
140
+ playerId: createPlayerId("p1"),
141
+ params: { winnerId: createPlayerId("p1") },
142
+ });
143
+
144
+ // Should not throw
145
+ expectGameEnd(engine, createPlayerId("p1"), "Player eliminated");
146
+ });
147
+
148
+ it("should throw when reason does not match", () => {
149
+ const engine = createTestEngine();
150
+
151
+ engine.executeMove("setWinner", {
152
+ playerId: createPlayerId("p1"),
153
+ params: { winnerId: createPlayerId("p1") },
154
+ });
155
+
156
+ expect(() => {
157
+ expectGameEnd(engine, createPlayerId("p1"), "Wrong reason");
158
+ }).toThrow(/Expected reason to be 'Wrong reason'/);
159
+ });
160
+
161
+ it("should return game end result", () => {
162
+ const engine = createTestEngine();
163
+
164
+ engine.executeMove("setWinner", {
165
+ playerId: createPlayerId("p1"),
166
+ params: { winnerId: createPlayerId("p1") },
167
+ });
168
+
169
+ const result = expectGameEnd(engine);
170
+ expect(result.winner).toBe(createPlayerId("p1"));
171
+ expect(result.reason).toBe("Player eliminated");
172
+ });
173
+
174
+ it("should work in realistic game scenario", () => {
175
+ const engine = createTestEngine();
176
+
177
+ // Damage player 2 until they lose
178
+ engine.executeMove("damagePlayer", {
179
+ playerId: createPlayerId("p1"),
180
+ params: { targetId: createPlayerId("p2"), amount: 5 },
181
+ });
182
+
183
+ expectGameNotEnded(engine); // Game should still be ongoing
184
+
185
+ engine.executeMove("damagePlayer", {
186
+ playerId: createPlayerId("p1"),
187
+ params: { targetId: createPlayerId("p2"), amount: 5 },
188
+ });
189
+
190
+ // Game should have ended
191
+ expectGameEnd(engine, createPlayerId("p1"), "Player eliminated");
192
+ });
193
+ });
194
+
195
+ describe("expectGameNotEnded", () => {
196
+ it("should pass when game has not ended", () => {
197
+ const engine = createTestEngine();
198
+
199
+ // Should not throw
200
+ expectGameNotEnded(engine);
201
+ });
202
+
203
+ it("should throw when game has ended", () => {
204
+ const engine = createTestEngine();
205
+
206
+ engine.executeMove("setWinner", {
207
+ playerId: createPlayerId("p1"),
208
+ params: { winnerId: createPlayerId("p1") },
209
+ });
210
+
211
+ expect(() => {
212
+ expectGameNotEnded(engine);
213
+ }).toThrow(/Expected game to still be ongoing/);
214
+ });
215
+
216
+ it("should provide helpful error message with end details", () => {
217
+ const engine = createTestEngine();
218
+
219
+ engine.executeMove("setWinner", {
220
+ playerId: createPlayerId("p1"),
221
+ params: { winnerId: createPlayerId("p1") },
222
+ });
223
+
224
+ expect(() => {
225
+ expectGameNotEnded(engine);
226
+ }).toThrow(/"winner":"p1"/);
227
+ });
228
+ });
229
+
230
+ describe("integration", () => {
231
+ it("should work together to test game flow", () => {
232
+ const engine = createTestEngine();
233
+
234
+ // Initially game is not ended
235
+ expectGameNotEnded(engine);
236
+
237
+ // Do some damage but not enough to end game
238
+ engine.executeMove("damagePlayer", {
239
+ playerId: createPlayerId("p1"),
240
+ params: { targetId: createPlayerId("p2"), amount: 3 },
241
+ });
242
+
243
+ expectGameNotEnded(engine);
244
+
245
+ // More damage
246
+ engine.executeMove("damagePlayer", {
247
+ playerId: createPlayerId("p1"),
248
+ params: { targetId: createPlayerId("p2"), amount: 3 },
249
+ });
250
+
251
+ expectGameNotEnded(engine);
252
+
253
+ // Final damage should end game
254
+ engine.executeMove("damagePlayer", {
255
+ playerId: createPlayerId("p1"),
256
+ params: { targetId: createPlayerId("p2"), amount: 4 },
257
+ });
258
+
259
+ expectGameEnd(engine, createPlayerId("p1"));
260
+ });
261
+ });
262
+ });
@@ -0,0 +1,95 @@
1
+ import type { RuleEngine } from "../engine/rule-engine";
2
+ import type { GameEndResult } from "../game-definition/game-definition";
3
+
4
+ /**
5
+ * Test End Assertions
6
+ *
7
+ * Assertion helpers for testing game end conditions
8
+ */
9
+
10
+ /**
11
+ * Assert that the game has ended
12
+ *
13
+ * Verifies that checkGameEnd returns a truthy value.
14
+ * Optionally checks the winner and/or reason.
15
+ *
16
+ * @param engine - Rule engine instance
17
+ * @param expectedWinner - Optional expected winner
18
+ * @param expectedReason - Optional expected reason
19
+ * @returns Game end result
20
+ * @throws Error if game hasn't ended or end result doesn't match expectations
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // Check that game ended
25
+ * expectGameEnd(engine);
26
+ *
27
+ * // Check specific winner
28
+ * expectGameEnd(engine, 'player1');
29
+ *
30
+ * // Check winner and reason
31
+ * expectGameEnd(engine, 'player1', 'Opponent eliminated');
32
+ * ```
33
+ */
34
+ export function expectGameEnd<TState, TMoves extends Record<string, any>>(
35
+ engine: RuleEngine<TState, TMoves>,
36
+ expectedWinner?: string,
37
+ expectedReason?: string,
38
+ ): GameEndResult {
39
+ const endResult = engine.checkGameEnd();
40
+
41
+ if (!endResult) {
42
+ throw new Error(
43
+ "Expected game to have ended, but checkGameEnd() returned undefined",
44
+ );
45
+ }
46
+
47
+ // Check winner if specified
48
+ if (expectedWinner !== undefined && endResult.winner !== expectedWinner) {
49
+ throw new Error(
50
+ `Expected winner to be '${expectedWinner}', but got '${endResult.winner}'`,
51
+ );
52
+ }
53
+
54
+ // Check reason if specified
55
+ if (expectedReason !== undefined && endResult.reason !== expectedReason) {
56
+ throw new Error(
57
+ `Expected reason to be '${expectedReason}', but got '${endResult.reason}'`,
58
+ );
59
+ }
60
+
61
+ return endResult;
62
+ }
63
+
64
+ /**
65
+ * Assert that the game has not ended
66
+ *
67
+ * Verifies that checkGameEnd returns undefined/falsy.
68
+ * Useful for testing that intermediate game states don't trigger end conditions.
69
+ *
70
+ * @param engine - Rule engine instance
71
+ * @throws Error if game has ended
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * // Verify game is still ongoing
76
+ * expectGameNotEnded(engine);
77
+ *
78
+ * // Do some moves
79
+ * engine.executeMove('attack', { ... });
80
+ *
81
+ * // Verify game still hasn't ended
82
+ * expectGameNotEnded(engine);
83
+ * ```
84
+ */
85
+ export function expectGameNotEnded<TState, TMoves extends Record<string, any>>(
86
+ engine: RuleEngine<TState, TMoves>,
87
+ ): void {
88
+ const endResult = engine.checkGameEnd();
89
+
90
+ if (endResult) {
91
+ throw new Error(
92
+ `Expected game to still be ongoing, but it ended with: ${JSON.stringify(endResult)}`,
93
+ );
94
+ }
95
+ }