@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.
- package/README.md +882 -0
- package/package.json +58 -0
- package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
- package/src/__tests__/createMockAlphaClashGame.ts +462 -0
- package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
- package/src/__tests__/createMockGundamGame.ts +379 -0
- package/src/__tests__/createMockLorcanaGame.ts +328 -0
- package/src/__tests__/createMockOnePieceGame.ts +429 -0
- package/src/__tests__/createMockRiftboundGame.ts +462 -0
- package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
- package/src/__tests__/gundam-engine-definition.test.ts +110 -0
- package/src/__tests__/integration-complete-game.test.ts +508 -0
- package/src/__tests__/integration-network-sync.test.ts +469 -0
- package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
- package/src/__tests__/move-enumeration.test.ts +725 -0
- package/src/__tests__/multiplayer-engine.test.ts +555 -0
- package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
- package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
- package/src/actions/action-definition.test.ts +201 -0
- package/src/actions/action-definition.ts +122 -0
- package/src/actions/action-timing.test.ts +490 -0
- package/src/actions/action-timing.ts +257 -0
- package/src/cards/card-definition.test.ts +268 -0
- package/src/cards/card-definition.ts +27 -0
- package/src/cards/card-instance.test.ts +422 -0
- package/src/cards/card-instance.ts +49 -0
- package/src/cards/computed-properties.test.ts +530 -0
- package/src/cards/computed-properties.ts +84 -0
- package/src/cards/conditional-modifiers.test.ts +390 -0
- package/src/cards/modifiers.test.ts +286 -0
- package/src/cards/modifiers.ts +51 -0
- package/src/engine/MULTIPLAYER.md +425 -0
- package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
- package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
- package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
- package/src/engine/__tests__/rule-engine.test.ts +366 -0
- package/src/engine/index.ts +14 -0
- package/src/engine/multiplayer-engine.example.ts +571 -0
- package/src/engine/multiplayer-engine.ts +409 -0
- package/src/engine/rule-engine.test.ts +286 -0
- package/src/engine/rule-engine.ts +1539 -0
- package/src/engine/tracker-system.ts +172 -0
- package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
- package/src/filtering/card-filter.test.ts +230 -0
- package/src/filtering/card-filter.ts +91 -0
- package/src/filtering/card-query.test.ts +901 -0
- package/src/filtering/card-query.ts +273 -0
- package/src/filtering/filter-matching.test.ts +944 -0
- package/src/filtering/filter-matching.ts +315 -0
- package/src/flow/SERIALIZATION.md +428 -0
- package/src/flow/__tests__/flow-definition.test.ts +427 -0
- package/src/flow/__tests__/flow-manager.test.ts +756 -0
- package/src/flow/__tests__/flow-serialization.test.ts +565 -0
- package/src/flow/flow-definition.ts +453 -0
- package/src/flow/flow-manager.ts +1044 -0
- package/src/flow/index.ts +35 -0
- package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
- package/src/game-definition/__tests__/game-definition.test.ts +291 -0
- package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
- package/src/game-definition/game-definition.ts +261 -0
- package/src/game-definition/index.ts +28 -0
- package/src/game-definition/move-definitions.ts +188 -0
- package/src/game-definition/validation.ts +183 -0
- package/src/history/history-manager.test.ts +497 -0
- package/src/history/history-manager.ts +312 -0
- package/src/history/history-operations.ts +122 -0
- package/src/history/index.ts +9 -0
- package/src/history/types.ts +255 -0
- package/src/index.ts +32 -0
- package/src/logging/index.ts +27 -0
- package/src/logging/log-formatter.ts +187 -0
- package/src/logging/logger.ts +276 -0
- package/src/logging/types.ts +148 -0
- package/src/moves/create-move.test.ts +331 -0
- package/src/moves/create-move.ts +64 -0
- package/src/moves/move-enumeration.ts +228 -0
- package/src/moves/move-executor.test.ts +431 -0
- package/src/moves/move-executor.ts +195 -0
- package/src/moves/move-system.test.ts +380 -0
- package/src/moves/move-system.ts +463 -0
- package/src/moves/standard-moves.ts +231 -0
- package/src/operations/card-operations.test.ts +236 -0
- package/src/operations/card-operations.ts +116 -0
- package/src/operations/card-registry-impl.test.ts +251 -0
- package/src/operations/card-registry-impl.ts +70 -0
- package/src/operations/card-registry.test.ts +234 -0
- package/src/operations/card-registry.ts +106 -0
- package/src/operations/counter-operations.ts +152 -0
- package/src/operations/game-operations.test.ts +280 -0
- package/src/operations/game-operations.ts +140 -0
- package/src/operations/index.ts +24 -0
- package/src/operations/operations-impl.test.ts +354 -0
- package/src/operations/operations-impl.ts +468 -0
- package/src/operations/zone-operations.test.ts +295 -0
- package/src/operations/zone-operations.ts +223 -0
- package/src/rng/seeded-rng.test.ts +339 -0
- package/src/rng/seeded-rng.ts +123 -0
- package/src/targeting/index.ts +48 -0
- package/src/targeting/target-definition.test.ts +273 -0
- package/src/targeting/target-definition.ts +37 -0
- package/src/targeting/target-dsl.ts +279 -0
- package/src/targeting/target-resolver.ts +486 -0
- package/src/targeting/target-validation.test.ts +994 -0
- package/src/targeting/target-validation.ts +286 -0
- package/src/telemetry/events.ts +202 -0
- package/src/telemetry/index.ts +21 -0
- package/src/telemetry/telemetry-manager.ts +127 -0
- package/src/telemetry/types.ts +68 -0
- package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
- package/src/testing/index.ts +88 -0
- package/src/testing/test-assertions.test.ts +341 -0
- package/src/testing/test-assertions.ts +256 -0
- package/src/testing/test-card-factory.test.ts +228 -0
- package/src/testing/test-card-factory.ts +111 -0
- package/src/testing/test-context-factory.ts +187 -0
- package/src/testing/test-end-assertions.test.ts +262 -0
- package/src/testing/test-end-assertions.ts +95 -0
- package/src/testing/test-engine-builder.test.ts +389 -0
- package/src/testing/test-engine-builder.ts +46 -0
- package/src/testing/test-flow-assertions.test.ts +284 -0
- package/src/testing/test-flow-assertions.ts +115 -0
- package/src/testing/test-player-builder.test.ts +132 -0
- package/src/testing/test-player-builder.ts +46 -0
- package/src/testing/test-replay-assertions.test.ts +356 -0
- package/src/testing/test-replay-assertions.ts +164 -0
- package/src/testing/test-rng-helpers.test.ts +260 -0
- package/src/testing/test-rng-helpers.ts +190 -0
- package/src/testing/test-state-builder.test.ts +373 -0
- package/src/testing/test-state-builder.ts +99 -0
- package/src/testing/test-zone-factory.test.ts +295 -0
- package/src/testing/test-zone-factory.ts +224 -0
- package/src/types/branded-utils.ts +54 -0
- package/src/types/branded.test.ts +175 -0
- package/src/types/branded.ts +33 -0
- package/src/types/index.ts +8 -0
- package/src/types/state.test.ts +198 -0
- package/src/types/state.ts +154 -0
- package/src/validation/card-type-guards.test.ts +242 -0
- package/src/validation/card-type-guards.ts +179 -0
- package/src/validation/index.ts +40 -0
- package/src/validation/schema-builders.test.ts +403 -0
- package/src/validation/schema-builders.ts +345 -0
- package/src/validation/type-guard-builder.test.ts +216 -0
- package/src/validation/type-guard-builder.ts +109 -0
- package/src/validation/validator-builder.test.ts +375 -0
- package/src/validation/validator-builder.ts +273 -0
- package/src/zones/index.ts +28 -0
- package/src/zones/zone-factory.test.ts +183 -0
- package/src/zones/zone-factory.ts +44 -0
- package/src/zones/zone-operations.test.ts +800 -0
- package/src/zones/zone-operations.ts +306 -0
- package/src/zones/zone-state-helpers.test.ts +337 -0
- package/src/zones/zone-state-helpers.ts +128 -0
- package/src/zones/zone-visibility.test.ts +156 -0
- package/src/zones/zone-visibility.ts +36 -0
- package/src/zones/zone.test.ts +186 -0
- package/src/zones/zone.ts +66 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Move Enumeration System Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the move enumeration functionality including:
|
|
5
|
+
* - Basic enumeration
|
|
6
|
+
* - Validation filtering
|
|
7
|
+
* - Metadata inclusion
|
|
8
|
+
* - Error handling
|
|
9
|
+
* - Complex parameter types
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "bun:test";
|
|
13
|
+
import { type GameDefinition, type Player, RuleEngine } from "../index";
|
|
14
|
+
import { createPlayerId } from "../types/branded-utils";
|
|
15
|
+
|
|
16
|
+
// Test game state
|
|
17
|
+
type TestGameState = {
|
|
18
|
+
players: Array<{
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
hand: string[];
|
|
22
|
+
field: string[];
|
|
23
|
+
mana: number;
|
|
24
|
+
}>;
|
|
25
|
+
currentPlayerIndex: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Test move parameters
|
|
29
|
+
type PlayCardParams = {
|
|
30
|
+
cardId: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type AttackParams = {
|
|
34
|
+
attackerId: string;
|
|
35
|
+
targetId: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type PassTurnParams = Record<string, never>;
|
|
39
|
+
|
|
40
|
+
type TestMoves = {
|
|
41
|
+
playCard: PlayCardParams;
|
|
42
|
+
attack: AttackParams;
|
|
43
|
+
passTurn: PassTurnParams;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe("Move Enumeration System", () => {
|
|
47
|
+
describe("Basic Enumeration", () => {
|
|
48
|
+
it("should enumerate moves with simple parameters", () => {
|
|
49
|
+
const players: Player[] = [
|
|
50
|
+
{ id: "p1", name: "Player 1" },
|
|
51
|
+
{ id: "p2", name: "Player 2" },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
55
|
+
name: "Test Game",
|
|
56
|
+
setup: (players) => ({
|
|
57
|
+
players: players.map((p) => ({
|
|
58
|
+
id: p.id,
|
|
59
|
+
name: p.name ?? "",
|
|
60
|
+
hand: ["card1", "card2", "card3"],
|
|
61
|
+
field: [],
|
|
62
|
+
mana: 5,
|
|
63
|
+
})),
|
|
64
|
+
currentPlayerIndex: 0,
|
|
65
|
+
}),
|
|
66
|
+
moves: {
|
|
67
|
+
playCard: {
|
|
68
|
+
enumerator: (
|
|
69
|
+
state: TestGameState,
|
|
70
|
+
context: import("../moves/move-enumeration").MoveEnumerationContext,
|
|
71
|
+
) => {
|
|
72
|
+
const player = state.players.find(
|
|
73
|
+
(p) => p.id === context.playerId,
|
|
74
|
+
);
|
|
75
|
+
if (!player) return [];
|
|
76
|
+
|
|
77
|
+
// Return all cards in hand as possible parameters
|
|
78
|
+
return player.hand.map((cardId: string) => ({ cardId }));
|
|
79
|
+
},
|
|
80
|
+
condition: (state, context) => {
|
|
81
|
+
const player = state.players.find(
|
|
82
|
+
(p) => p.id === context.playerId,
|
|
83
|
+
);
|
|
84
|
+
if (!player) return false;
|
|
85
|
+
|
|
86
|
+
// Check if card is in hand
|
|
87
|
+
return player.hand.includes(context.params.cardId);
|
|
88
|
+
},
|
|
89
|
+
reducer: (draft, context) => {
|
|
90
|
+
const player = draft.players.find(
|
|
91
|
+
(p) => p.id === context.playerId,
|
|
92
|
+
);
|
|
93
|
+
if (!player) return;
|
|
94
|
+
|
|
95
|
+
// Move card from hand to field
|
|
96
|
+
const index = player.hand.indexOf(context.params.cardId);
|
|
97
|
+
if (index >= 0) {
|
|
98
|
+
player.hand.splice(index, 1);
|
|
99
|
+
player.field.push(context.params.cardId);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
attack: {
|
|
104
|
+
enumerator: () => [],
|
|
105
|
+
condition: () => false,
|
|
106
|
+
reducer: () => {},
|
|
107
|
+
},
|
|
108
|
+
passTurn: {
|
|
109
|
+
enumerator: () => [{}],
|
|
110
|
+
condition: () => true,
|
|
111
|
+
reducer: () => {},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
117
|
+
const playerId = createPlayerId("p1");
|
|
118
|
+
|
|
119
|
+
// Enumerate all moves
|
|
120
|
+
const moves = engine.enumerateMoves(playerId);
|
|
121
|
+
|
|
122
|
+
// Should have moves for: playCard (3 cards) + attack (0) + passTurn (1)
|
|
123
|
+
const playCardMoves = moves.filter((m) => m.moveId === "playCard");
|
|
124
|
+
const passMoves = moves.filter((m) => m.moveId === "passTurn");
|
|
125
|
+
|
|
126
|
+
expect(playCardMoves.length).toBe(3);
|
|
127
|
+
expect(passMoves.length).toBe(1);
|
|
128
|
+
|
|
129
|
+
// Check that all cards are enumerated
|
|
130
|
+
const cardIds = playCardMoves.map(
|
|
131
|
+
(m) => (m.params as PlayCardParams).cardId,
|
|
132
|
+
);
|
|
133
|
+
expect(cardIds).toContain("card1");
|
|
134
|
+
expect(cardIds).toContain("card2");
|
|
135
|
+
expect(cardIds).toContain("card3");
|
|
136
|
+
|
|
137
|
+
// All should be valid
|
|
138
|
+
for (const move of playCardMoves) {
|
|
139
|
+
expect(move.isValid).toBe(true);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should enumerate moves without parameters", () => {
|
|
144
|
+
const players: Player[] = [{ id: "p1", name: "Player 1" }];
|
|
145
|
+
|
|
146
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
147
|
+
name: "Test Game",
|
|
148
|
+
setup: () => ({
|
|
149
|
+
players: [
|
|
150
|
+
{ id: "p1", name: "Player 1", hand: [], field: [], mana: 0 },
|
|
151
|
+
],
|
|
152
|
+
currentPlayerIndex: 0,
|
|
153
|
+
}),
|
|
154
|
+
moves: {
|
|
155
|
+
playCard: {
|
|
156
|
+
enumerator: () => [],
|
|
157
|
+
condition: () => false,
|
|
158
|
+
reducer: () => {},
|
|
159
|
+
},
|
|
160
|
+
attack: {
|
|
161
|
+
enumerator: () => [],
|
|
162
|
+
condition: () => false,
|
|
163
|
+
reducer: () => {},
|
|
164
|
+
},
|
|
165
|
+
passTurn: {
|
|
166
|
+
// Enumerator returns single empty object for moves without params
|
|
167
|
+
enumerator: () => [{}],
|
|
168
|
+
condition: () => true,
|
|
169
|
+
reducer: () => {},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
175
|
+
const playerId = createPlayerId("p1");
|
|
176
|
+
|
|
177
|
+
const moves = engine.enumerateMoves(playerId, { validOnly: true });
|
|
178
|
+
|
|
179
|
+
// Only passTurn should be valid
|
|
180
|
+
expect(moves.length).toBe(1);
|
|
181
|
+
expect(moves[0]?.moveId).toBe("passTurn");
|
|
182
|
+
expect(moves[0]?.params).toEqual({});
|
|
183
|
+
expect(moves[0]?.isValid).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("Validation Filtering", () => {
|
|
188
|
+
it("should filter by validOnly option", () => {
|
|
189
|
+
const players: Player[] = [{ id: "p1", name: "Player 1" }];
|
|
190
|
+
|
|
191
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
192
|
+
name: "Test Game",
|
|
193
|
+
setup: () => ({
|
|
194
|
+
players: [
|
|
195
|
+
{ id: "p1", name: "Player 1", hand: ["card1"], field: [], mana: 0 },
|
|
196
|
+
],
|
|
197
|
+
currentPlayerIndex: 0,
|
|
198
|
+
}),
|
|
199
|
+
moves: {
|
|
200
|
+
playCard: {
|
|
201
|
+
enumerator: (
|
|
202
|
+
state: TestGameState,
|
|
203
|
+
context: import("../moves/move-enumeration").MoveEnumerationContext,
|
|
204
|
+
) => {
|
|
205
|
+
const player = state.players.find(
|
|
206
|
+
(p) => p.id === context.playerId,
|
|
207
|
+
);
|
|
208
|
+
return player
|
|
209
|
+
? player.hand.map((cardId: string) => ({ cardId }))
|
|
210
|
+
: [];
|
|
211
|
+
},
|
|
212
|
+
// Condition requires mana (which player doesn't have)
|
|
213
|
+
condition: (state, context) => {
|
|
214
|
+
const player = state.players.find(
|
|
215
|
+
(p) => p.id === context.playerId,
|
|
216
|
+
);
|
|
217
|
+
return (player?.mana ?? 0) > 0;
|
|
218
|
+
},
|
|
219
|
+
reducer: () => {},
|
|
220
|
+
},
|
|
221
|
+
attack: {
|
|
222
|
+
enumerator: () => [],
|
|
223
|
+
condition: () => false,
|
|
224
|
+
reducer: () => {},
|
|
225
|
+
},
|
|
226
|
+
passTurn: {
|
|
227
|
+
enumerator: () => [{}],
|
|
228
|
+
condition: () => true,
|
|
229
|
+
reducer: () => {},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
235
|
+
const playerId = createPlayerId("p1");
|
|
236
|
+
|
|
237
|
+
// Get all moves (including invalid)
|
|
238
|
+
const allMoves = engine.enumerateMoves(playerId, { validOnly: false });
|
|
239
|
+
expect(allMoves.length).toBeGreaterThan(1);
|
|
240
|
+
|
|
241
|
+
// Check that playCard exists but is invalid
|
|
242
|
+
const playCardMove = allMoves.find((m) => m.moveId === "playCard");
|
|
243
|
+
expect(playCardMove).toBeDefined();
|
|
244
|
+
expect(playCardMove?.isValid).toBe(false);
|
|
245
|
+
expect(playCardMove?.validationError).toBeDefined();
|
|
246
|
+
|
|
247
|
+
// Get only valid moves
|
|
248
|
+
const validMoves = engine.enumerateMoves(playerId, { validOnly: true });
|
|
249
|
+
|
|
250
|
+
// Only passTurn should be valid
|
|
251
|
+
expect(validMoves.length).toBe(1);
|
|
252
|
+
expect(validMoves[0]?.moveId).toBe("passTurn");
|
|
253
|
+
expect(validMoves[0]?.isValid).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("should include validation error details", () => {
|
|
257
|
+
const players: Player[] = [{ id: "p1", name: "Player 1" }];
|
|
258
|
+
|
|
259
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
260
|
+
name: "Test Game",
|
|
261
|
+
setup: () => ({
|
|
262
|
+
players: [
|
|
263
|
+
{ id: "p1", name: "Player 1", hand: ["card1"], field: [], mana: 0 },
|
|
264
|
+
],
|
|
265
|
+
currentPlayerIndex: 0,
|
|
266
|
+
}),
|
|
267
|
+
moves: {
|
|
268
|
+
playCard: {
|
|
269
|
+
enumerator: (
|
|
270
|
+
state: TestGameState,
|
|
271
|
+
context: import("../moves/move-enumeration").MoveEnumerationContext,
|
|
272
|
+
) => {
|
|
273
|
+
const player = state.players.find(
|
|
274
|
+
(p) => p.id === context.playerId,
|
|
275
|
+
);
|
|
276
|
+
return player
|
|
277
|
+
? player.hand.map((cardId: string) => ({ cardId }))
|
|
278
|
+
: [];
|
|
279
|
+
},
|
|
280
|
+
// Return detailed failure information
|
|
281
|
+
condition: (state, context) => {
|
|
282
|
+
const player = state.players.find(
|
|
283
|
+
(p) => p.id === context.playerId,
|
|
284
|
+
);
|
|
285
|
+
const required = 5;
|
|
286
|
+
const available = player?.mana ?? 0;
|
|
287
|
+
|
|
288
|
+
if (available < required) {
|
|
289
|
+
return {
|
|
290
|
+
reason: `Not enough mana. Required: ${required}, Available: ${available}`,
|
|
291
|
+
errorCode: "INSUFFICIENT_MANA",
|
|
292
|
+
context: { required, available },
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return true;
|
|
297
|
+
},
|
|
298
|
+
reducer: () => {},
|
|
299
|
+
},
|
|
300
|
+
attack: {
|
|
301
|
+
enumerator: () => [],
|
|
302
|
+
condition: () => false,
|
|
303
|
+
reducer: () => {},
|
|
304
|
+
},
|
|
305
|
+
passTurn: {
|
|
306
|
+
enumerator: () => [{}],
|
|
307
|
+
condition: () => true,
|
|
308
|
+
reducer: () => {},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
314
|
+
const playerId = createPlayerId("p1");
|
|
315
|
+
|
|
316
|
+
const moves = engine.enumerateMoves(playerId, { validOnly: false });
|
|
317
|
+
const playCardMove = moves.find((m) => m.moveId === "playCard");
|
|
318
|
+
|
|
319
|
+
expect(playCardMove).toBeDefined();
|
|
320
|
+
expect(playCardMove?.isValid).toBe(false);
|
|
321
|
+
expect(playCardMove?.validationError).toBeDefined();
|
|
322
|
+
expect(playCardMove?.validationError?.errorCode).toBe(
|
|
323
|
+
"INSUFFICIENT_MANA",
|
|
324
|
+
);
|
|
325
|
+
expect(playCardMove?.validationError?.reason).toContain(
|
|
326
|
+
"Not enough mana",
|
|
327
|
+
);
|
|
328
|
+
expect(playCardMove?.validationError?.context).toEqual({
|
|
329
|
+
required: 5,
|
|
330
|
+
available: 0,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("Metadata Inclusion", () => {
|
|
336
|
+
it("should include metadata when requested", () => {
|
|
337
|
+
const players: Player[] = [{ id: "p1", name: "Player 1" }];
|
|
338
|
+
|
|
339
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
340
|
+
name: "Test Game",
|
|
341
|
+
setup: () => ({
|
|
342
|
+
players: [
|
|
343
|
+
{ id: "p1", name: "Player 1", hand: ["card1"], field: [], mana: 5 },
|
|
344
|
+
],
|
|
345
|
+
currentPlayerIndex: 0,
|
|
346
|
+
}),
|
|
347
|
+
moves: {
|
|
348
|
+
playCard: {
|
|
349
|
+
enumerator: (
|
|
350
|
+
state: TestGameState,
|
|
351
|
+
context: import("../moves/move-enumeration").MoveEnumerationContext,
|
|
352
|
+
) => {
|
|
353
|
+
const player = state.players.find(
|
|
354
|
+
(p) => p.id === context.playerId,
|
|
355
|
+
);
|
|
356
|
+
return player
|
|
357
|
+
? player.hand.map((cardId: string) => ({ cardId }))
|
|
358
|
+
: [];
|
|
359
|
+
},
|
|
360
|
+
condition: () => true,
|
|
361
|
+
reducer: () => {},
|
|
362
|
+
metadata: {
|
|
363
|
+
displayName: "Play Card",
|
|
364
|
+
description: "Play a card from your hand",
|
|
365
|
+
category: "action",
|
|
366
|
+
tags: ["card", "play"],
|
|
367
|
+
priority: 1,
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
attack: {
|
|
371
|
+
enumerator: () => [],
|
|
372
|
+
condition: () => false,
|
|
373
|
+
reducer: () => {},
|
|
374
|
+
},
|
|
375
|
+
passTurn: {
|
|
376
|
+
enumerator: () => [{}],
|
|
377
|
+
condition: () => true,
|
|
378
|
+
reducer: () => {},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
384
|
+
const playerId = createPlayerId("p1");
|
|
385
|
+
|
|
386
|
+
// Without metadata
|
|
387
|
+
const movesWithoutMeta = engine.enumerateMoves(playerId, {
|
|
388
|
+
includeMetadata: false,
|
|
389
|
+
});
|
|
390
|
+
const playCardWithoutMeta = movesWithoutMeta.find(
|
|
391
|
+
(m) => m.moveId === "playCard",
|
|
392
|
+
);
|
|
393
|
+
expect(playCardWithoutMeta?.metadata).toBeUndefined();
|
|
394
|
+
|
|
395
|
+
// With metadata
|
|
396
|
+
const movesWithMeta = engine.enumerateMoves(playerId, {
|
|
397
|
+
includeMetadata: true,
|
|
398
|
+
});
|
|
399
|
+
const playCardWithMeta = movesWithMeta.find(
|
|
400
|
+
(m) => m.moveId === "playCard",
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(playCardWithMeta?.metadata).toBeDefined();
|
|
404
|
+
expect(playCardWithMeta?.metadata?.displayName).toBe("Play Card");
|
|
405
|
+
expect(playCardWithMeta?.metadata?.description).toBe(
|
|
406
|
+
"Play a card from your hand",
|
|
407
|
+
);
|
|
408
|
+
expect(playCardWithMeta?.metadata?.category).toBe("action");
|
|
409
|
+
expect(playCardWithMeta?.metadata?.tags).toEqual(["card", "play"]);
|
|
410
|
+
expect(playCardWithMeta?.metadata?.priority).toBe(1);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("Moves Without Enumerators", () => {
|
|
415
|
+
it("should handle moves without enumerators", () => {
|
|
416
|
+
const players: Player[] = [{ id: "p1", name: "Player 1" }];
|
|
417
|
+
|
|
418
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
419
|
+
name: "Test Game",
|
|
420
|
+
setup: () => ({
|
|
421
|
+
players: [
|
|
422
|
+
{ id: "p1", name: "Player 1", hand: [], field: [], mana: 0 },
|
|
423
|
+
],
|
|
424
|
+
currentPlayerIndex: 0,
|
|
425
|
+
}),
|
|
426
|
+
moves: {
|
|
427
|
+
playCard: {
|
|
428
|
+
// No enumerator provided
|
|
429
|
+
condition: () => true,
|
|
430
|
+
reducer: () => {},
|
|
431
|
+
},
|
|
432
|
+
attack: {
|
|
433
|
+
enumerator: () => [],
|
|
434
|
+
condition: () => false,
|
|
435
|
+
reducer: () => {},
|
|
436
|
+
},
|
|
437
|
+
passTurn: {
|
|
438
|
+
enumerator: () => [{}],
|
|
439
|
+
condition: () => true,
|
|
440
|
+
reducer: () => {},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
446
|
+
const playerId = createPlayerId("p1");
|
|
447
|
+
|
|
448
|
+
// With validOnly: false, should include move with error
|
|
449
|
+
const allMoves = engine.enumerateMoves(playerId, { validOnly: false });
|
|
450
|
+
const playCardMove = allMoves.find((m) => m.moveId === "playCard");
|
|
451
|
+
|
|
452
|
+
expect(playCardMove).toBeDefined();
|
|
453
|
+
expect(playCardMove?.isValid).toBe(false);
|
|
454
|
+
expect(playCardMove?.validationError?.errorCode).toBe("NO_ENUMERATOR");
|
|
455
|
+
expect(playCardMove?.validationError?.reason).toContain(
|
|
456
|
+
"no enumerator provided",
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// With validOnly: true, should not include move
|
|
460
|
+
const validMoves = engine.enumerateMoves(playerId, { validOnly: true });
|
|
461
|
+
const playCardInValid = validMoves.find((m) => m.moveId === "playCard");
|
|
462
|
+
expect(playCardInValid).toBeUndefined();
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe("Error Handling", () => {
|
|
467
|
+
it("should handle enumerator errors gracefully", () => {
|
|
468
|
+
const players: Player[] = [{ id: "p1", name: "Player 1" }];
|
|
469
|
+
|
|
470
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
471
|
+
name: "Test Game",
|
|
472
|
+
setup: () => ({
|
|
473
|
+
players: [
|
|
474
|
+
{ id: "p1", name: "Player 1", hand: [], field: [], mana: 0 },
|
|
475
|
+
],
|
|
476
|
+
currentPlayerIndex: 0,
|
|
477
|
+
}),
|
|
478
|
+
moves: {
|
|
479
|
+
playCard: {
|
|
480
|
+
// Enumerator that throws an error
|
|
481
|
+
enumerator: () => {
|
|
482
|
+
throw new Error("Test enumerator error");
|
|
483
|
+
},
|
|
484
|
+
condition: () => true,
|
|
485
|
+
reducer: () => {},
|
|
486
|
+
},
|
|
487
|
+
attack: {
|
|
488
|
+
enumerator: () => [],
|
|
489
|
+
condition: () => false,
|
|
490
|
+
reducer: () => {},
|
|
491
|
+
},
|
|
492
|
+
passTurn: {
|
|
493
|
+
enumerator: () => [{}],
|
|
494
|
+
condition: () => true,
|
|
495
|
+
reducer: () => {},
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const engine = new RuleEngine(gameDefinition, players, {
|
|
501
|
+
logger: { level: "SILENT" }, // Suppress error logs in test
|
|
502
|
+
});
|
|
503
|
+
const playerId = createPlayerId("p1");
|
|
504
|
+
|
|
505
|
+
// Should not throw, but include error result
|
|
506
|
+
const moves = engine.enumerateMoves(playerId, { validOnly: false });
|
|
507
|
+
const playCardMove = moves.find((m) => m.moveId === "playCard");
|
|
508
|
+
|
|
509
|
+
expect(playCardMove).toBeDefined();
|
|
510
|
+
expect(playCardMove?.isValid).toBe(false);
|
|
511
|
+
expect(playCardMove?.validationError?.errorCode).toBe("ENUMERATOR_ERROR");
|
|
512
|
+
expect(playCardMove?.validationError?.reason).toContain(
|
|
513
|
+
"Test enumerator error",
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe("Filtering Options", () => {
|
|
519
|
+
it("should filter by moveIds", () => {
|
|
520
|
+
const players: Player[] = [{ id: "p1", name: "Player 1" }];
|
|
521
|
+
|
|
522
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
523
|
+
name: "Test Game",
|
|
524
|
+
setup: () => ({
|
|
525
|
+
players: [
|
|
526
|
+
{ id: "p1", name: "Player 1", hand: ["card1"], field: [], mana: 5 },
|
|
527
|
+
],
|
|
528
|
+
currentPlayerIndex: 0,
|
|
529
|
+
}),
|
|
530
|
+
moves: {
|
|
531
|
+
playCard: {
|
|
532
|
+
enumerator: (
|
|
533
|
+
state: TestGameState,
|
|
534
|
+
context: import("../moves/move-enumeration").MoveEnumerationContext,
|
|
535
|
+
) => {
|
|
536
|
+
const player = state.players.find(
|
|
537
|
+
(p) => p.id === context.playerId,
|
|
538
|
+
);
|
|
539
|
+
return player
|
|
540
|
+
? player.hand.map((cardId: string) => ({ cardId }))
|
|
541
|
+
: [];
|
|
542
|
+
},
|
|
543
|
+
condition: () => true,
|
|
544
|
+
reducer: () => {},
|
|
545
|
+
},
|
|
546
|
+
attack: {
|
|
547
|
+
enumerator: () => [{ attackerId: "a1", targetId: "t1" }],
|
|
548
|
+
condition: () => true,
|
|
549
|
+
reducer: () => {},
|
|
550
|
+
},
|
|
551
|
+
passTurn: {
|
|
552
|
+
enumerator: () => [{}],
|
|
553
|
+
condition: () => true,
|
|
554
|
+
reducer: () => {},
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
560
|
+
const playerId = createPlayerId("p1");
|
|
561
|
+
|
|
562
|
+
// Filter to only playCard and passTurn
|
|
563
|
+
const filteredMoves = engine.enumerateMoves(playerId, {
|
|
564
|
+
moveIds: ["playCard", "passTurn"],
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const moveIds = filteredMoves.map((m) => m.moveId);
|
|
568
|
+
expect(moveIds).toContain("playCard");
|
|
569
|
+
expect(moveIds).toContain("passTurn");
|
|
570
|
+
expect(moveIds).not.toContain("attack");
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("should limit results per move with maxPerMove", () => {
|
|
574
|
+
const players: Player[] = [{ id: "p1", name: "Player 1" }];
|
|
575
|
+
|
|
576
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
577
|
+
name: "Test Game",
|
|
578
|
+
setup: () => ({
|
|
579
|
+
players: [
|
|
580
|
+
{
|
|
581
|
+
id: "p1",
|
|
582
|
+
name: "Player 1",
|
|
583
|
+
hand: ["card1", "card2", "card3", "card4", "card5"],
|
|
584
|
+
field: [],
|
|
585
|
+
mana: 5,
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
currentPlayerIndex: 0,
|
|
589
|
+
}),
|
|
590
|
+
moves: {
|
|
591
|
+
playCard: {
|
|
592
|
+
enumerator: (
|
|
593
|
+
state: TestGameState,
|
|
594
|
+
context: import("../moves/move-enumeration").MoveEnumerationContext,
|
|
595
|
+
) => {
|
|
596
|
+
const player = state.players.find(
|
|
597
|
+
(p) => p.id === context.playerId,
|
|
598
|
+
);
|
|
599
|
+
return player
|
|
600
|
+
? player.hand.map((cardId: string) => ({ cardId }))
|
|
601
|
+
: [];
|
|
602
|
+
},
|
|
603
|
+
condition: () => true,
|
|
604
|
+
reducer: () => {},
|
|
605
|
+
},
|
|
606
|
+
attack: {
|
|
607
|
+
enumerator: () => [],
|
|
608
|
+
condition: () => false,
|
|
609
|
+
reducer: () => {},
|
|
610
|
+
},
|
|
611
|
+
passTurn: {
|
|
612
|
+
enumerator: () => [{}],
|
|
613
|
+
condition: () => true,
|
|
614
|
+
reducer: () => {},
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
620
|
+
const playerId = createPlayerId("p1");
|
|
621
|
+
|
|
622
|
+
// Limit to 2 results per move
|
|
623
|
+
const limitedMoves = engine.enumerateMoves(playerId, {
|
|
624
|
+
maxPerMove: 2,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const playCardMoves = limitedMoves.filter((m) => m.moveId === "playCard");
|
|
628
|
+
expect(playCardMoves.length).toBe(2); // Limited to 2 instead of 5
|
|
629
|
+
|
|
630
|
+
// Without limit
|
|
631
|
+
const allMoves = engine.enumerateMoves(playerId);
|
|
632
|
+
const allPlayCardMoves = allMoves.filter((m) => m.moveId === "playCard");
|
|
633
|
+
expect(allPlayCardMoves.length).toBe(5); // All 5 cards
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("Complex Parameter Types", () => {
|
|
638
|
+
it("should enumerate moves with multiple parameter fields", () => {
|
|
639
|
+
const players: Player[] = [
|
|
640
|
+
{ id: "p1", name: "Player 1" },
|
|
641
|
+
{ id: "p2", name: "Player 2" },
|
|
642
|
+
];
|
|
643
|
+
|
|
644
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
645
|
+
name: "Test Game",
|
|
646
|
+
setup: (players) => ({
|
|
647
|
+
players: players.map((p) => ({
|
|
648
|
+
id: p.id,
|
|
649
|
+
name: p.name ?? "",
|
|
650
|
+
hand: [],
|
|
651
|
+
field: p.id === "p1" ? ["attacker1", "attacker2"] : ["target1"],
|
|
652
|
+
mana: 5,
|
|
653
|
+
})),
|
|
654
|
+
currentPlayerIndex: 0,
|
|
655
|
+
}),
|
|
656
|
+
moves: {
|
|
657
|
+
playCard: {
|
|
658
|
+
enumerator: () => [],
|
|
659
|
+
condition: () => false,
|
|
660
|
+
reducer: () => {},
|
|
661
|
+
},
|
|
662
|
+
attack: {
|
|
663
|
+
// Enumerate all attacker-target combinations
|
|
664
|
+
enumerator: (
|
|
665
|
+
state: TestGameState,
|
|
666
|
+
context: import("../moves/move-enumeration").MoveEnumerationContext,
|
|
667
|
+
) => {
|
|
668
|
+
const results: AttackParams[] = [];
|
|
669
|
+
const player = state.players.find(
|
|
670
|
+
(p) => p.id === context.playerId,
|
|
671
|
+
);
|
|
672
|
+
if (!player) return [];
|
|
673
|
+
|
|
674
|
+
// Get all opponent creatures
|
|
675
|
+
const opponents = state.players.filter(
|
|
676
|
+
(p) => p.id !== context.playerId,
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
for (const attackerId of player.field) {
|
|
680
|
+
for (const opponent of opponents) {
|
|
681
|
+
for (const targetId of opponent.field) {
|
|
682
|
+
results.push({ attackerId, targetId });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return results;
|
|
688
|
+
},
|
|
689
|
+
condition: () => true,
|
|
690
|
+
reducer: () => {},
|
|
691
|
+
},
|
|
692
|
+
passTurn: {
|
|
693
|
+
enumerator: () => [{}],
|
|
694
|
+
condition: () => true,
|
|
695
|
+
reducer: () => {},
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
701
|
+
const playerId = createPlayerId("p1");
|
|
702
|
+
|
|
703
|
+
const moves = engine.enumerateMoves(playerId, { validOnly: true });
|
|
704
|
+
const attackMoves = moves.filter((m) => m.moveId === "attack");
|
|
705
|
+
|
|
706
|
+
// Should have 2 attackers * 1 target = 2 attack combinations
|
|
707
|
+
expect(attackMoves.length).toBe(2);
|
|
708
|
+
|
|
709
|
+
// Check all combinations are present
|
|
710
|
+
const combinations = attackMoves.map((m) => ({
|
|
711
|
+
attacker: (m.params as AttackParams).attackerId,
|
|
712
|
+
target: (m.params as AttackParams).targetId,
|
|
713
|
+
}));
|
|
714
|
+
|
|
715
|
+
expect(combinations).toContainEqual({
|
|
716
|
+
attacker: "attacker1",
|
|
717
|
+
target: "target1",
|
|
718
|
+
});
|
|
719
|
+
expect(combinations).toContainEqual({
|
|
720
|
+
attacker: "attacker2",
|
|
721
|
+
target: "target1",
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
});
|