@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,331 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { produce } from "immer";
|
|
3
|
+
import { createMockContext } from "../testing/test-context-factory";
|
|
4
|
+
import type { CardId, PlayerId } from "../types";
|
|
5
|
+
import { createCardId, createPlayerId } from "../types";
|
|
6
|
+
import { createMove } from "./create-move";
|
|
7
|
+
import type { MoveContext } from "./move-system";
|
|
8
|
+
|
|
9
|
+
describe("createMove Helper", () => {
|
|
10
|
+
/**
|
|
11
|
+
* Test game state structure
|
|
12
|
+
*/
|
|
13
|
+
type TestGameState = {
|
|
14
|
+
players: Record<PlayerId, { health: number; mana: number }>;
|
|
15
|
+
cards: Record<CardId, { name: string; damage: number }>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Test move parameter types - using a record type like real game implementations
|
|
20
|
+
*/
|
|
21
|
+
type TestMoveParams = {
|
|
22
|
+
// Move with specific params
|
|
23
|
+
quest: { cardId: CardId };
|
|
24
|
+
// Move with multiple params
|
|
25
|
+
challenge: { attackerId: CardId; defenderId: CardId };
|
|
26
|
+
// Move with optional params
|
|
27
|
+
playCard: { cardId: CardId; alternativeCost?: number };
|
|
28
|
+
// Move with no params
|
|
29
|
+
pass: Record<string, never>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const player1 = createPlayerId("p1");
|
|
33
|
+
const player2 = createPlayerId("p2");
|
|
34
|
+
const card1 = createCardId("card1");
|
|
35
|
+
const card2 = createCardId("card2");
|
|
36
|
+
|
|
37
|
+
const initialState: TestGameState = {
|
|
38
|
+
players: {
|
|
39
|
+
[player1]: { health: 20, mana: 5 },
|
|
40
|
+
[player2]: { health: 20, mana: 5 },
|
|
41
|
+
},
|
|
42
|
+
cards: {
|
|
43
|
+
[card1]: { name: "Character A", damage: 0 },
|
|
44
|
+
[card2]: { name: "Character B", damage: 0 },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe("Type Narrowing", () => {
|
|
49
|
+
it("should narrow params for single-param move", () => {
|
|
50
|
+
// Create a move with narrowed params using createMove
|
|
51
|
+
const questMove = createMove<
|
|
52
|
+
TestGameState,
|
|
53
|
+
TestMoveParams,
|
|
54
|
+
"quest" // ✅ Narrows to TestMoveParams["quest"]
|
|
55
|
+
>({
|
|
56
|
+
condition: (state, context) => {
|
|
57
|
+
// ✅ context.params is { cardId: CardId }
|
|
58
|
+
const { cardId } = context.params;
|
|
59
|
+
return !!state.cards[cardId];
|
|
60
|
+
},
|
|
61
|
+
reducer: (draft, context) => {
|
|
62
|
+
// ✅ context.params is { cardId: CardId }
|
|
63
|
+
const { cardId } = context.params;
|
|
64
|
+
const card = draft.cards[cardId];
|
|
65
|
+
if (card) {
|
|
66
|
+
card.damage += 1;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Verify the move works correctly
|
|
72
|
+
const context: MoveContext<TestMoveParams["quest"]> = createMockContext({
|
|
73
|
+
playerId: player1,
|
|
74
|
+
params: { cardId: card1 },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Test condition
|
|
78
|
+
expect(questMove.condition?.(initialState, context)).toBe(true);
|
|
79
|
+
|
|
80
|
+
// Test reducer
|
|
81
|
+
const nextState = produce(initialState, (draft) => {
|
|
82
|
+
questMove.reducer(draft, context);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(nextState.cards[card1].damage).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should narrow params for multi-param move", () => {
|
|
89
|
+
// Create a move with multiple params
|
|
90
|
+
const challengeMove = createMove<
|
|
91
|
+
TestGameState,
|
|
92
|
+
TestMoveParams,
|
|
93
|
+
"challenge" // ✅ Narrows to TestMoveParams["challenge"]
|
|
94
|
+
>({
|
|
95
|
+
condition: (state, context) => {
|
|
96
|
+
// ✅ context.params is { attackerId: CardId; defenderId: CardId }
|
|
97
|
+
const { attackerId, defenderId } = context.params;
|
|
98
|
+
return !!state.cards[attackerId] && !!state.cards[defenderId];
|
|
99
|
+
},
|
|
100
|
+
reducer: (draft, context) => {
|
|
101
|
+
// ✅ context.params is { attackerId: CardId; defenderId: CardId }
|
|
102
|
+
const { attackerId, defenderId } = context.params;
|
|
103
|
+
const attacker = draft.cards[attackerId];
|
|
104
|
+
const defender = draft.cards[defenderId];
|
|
105
|
+
if (attacker && defender) {
|
|
106
|
+
defender.damage += 3;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const context: MoveContext<TestMoveParams["challenge"]> =
|
|
112
|
+
createMockContext({
|
|
113
|
+
playerId: player1,
|
|
114
|
+
params: { attackerId: card1, defenderId: card2 },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(challengeMove.condition?.(initialState, context)).toBe(true);
|
|
118
|
+
|
|
119
|
+
const nextState = produce(initialState, (draft) => {
|
|
120
|
+
challengeMove.reducer(draft, context);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(nextState.cards[card2].damage).toBe(3);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should narrow params for move with optional fields", () => {
|
|
127
|
+
// Create a move with optional params
|
|
128
|
+
const playCardMove = createMove<
|
|
129
|
+
TestGameState,
|
|
130
|
+
TestMoveParams,
|
|
131
|
+
"playCard" // ✅ Narrows to TestMoveParams["playCard"]
|
|
132
|
+
>({
|
|
133
|
+
reducer: (draft, context) => {
|
|
134
|
+
// ✅ context.params is { cardId: CardId; alternativeCost?: number }
|
|
135
|
+
const { cardId, alternativeCost } = context.params;
|
|
136
|
+
const player = draft.players[context.playerId];
|
|
137
|
+
const cost = alternativeCost ?? 3;
|
|
138
|
+
player.mana -= cost;
|
|
139
|
+
|
|
140
|
+
// Use cardId (verify it's available)
|
|
141
|
+
const card = draft.cards[cardId];
|
|
142
|
+
if (card) {
|
|
143
|
+
card.damage = 0; // Reset damage on play
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Test with alternativeCost
|
|
149
|
+
const contextWithAlt: MoveContext<TestMoveParams["playCard"]> =
|
|
150
|
+
createMockContext({
|
|
151
|
+
playerId: player1,
|
|
152
|
+
params: { cardId: card1, alternativeCost: 2 },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const nextState1 = produce(initialState, (draft) => {
|
|
156
|
+
playCardMove.reducer(draft, contextWithAlt);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(nextState1.players[player1].mana).toBe(3); // 5 - 2
|
|
160
|
+
|
|
161
|
+
// Test without alternativeCost
|
|
162
|
+
const contextWithoutAlt: MoveContext<TestMoveParams["playCard"]> =
|
|
163
|
+
createMockContext({
|
|
164
|
+
playerId: player1,
|
|
165
|
+
params: { cardId: card1 }, // alternativeCost is optional
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const nextState2 = produce(initialState, (draft) => {
|
|
169
|
+
playCardMove.reducer(draft, contextWithoutAlt);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(nextState2.players[player1].mana).toBe(2); // 5 - 3 (default)
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should narrow params for no-param move", () => {
|
|
176
|
+
// Create a move with no params
|
|
177
|
+
const passMove = createMove<
|
|
178
|
+
TestGameState,
|
|
179
|
+
TestMoveParams,
|
|
180
|
+
"pass" // ✅ Narrows to TestMoveParams["pass"] (empty object)
|
|
181
|
+
>({
|
|
182
|
+
reducer: (draft, context) => {
|
|
183
|
+
// ✅ context.params is {} (empty object)
|
|
184
|
+
const player = draft.players[context.playerId];
|
|
185
|
+
player.mana = 10; // Reset mana on pass
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const context: MoveContext<TestMoveParams["pass"]> = createMockContext({
|
|
190
|
+
playerId: player1,
|
|
191
|
+
params: {},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const nextState = produce(initialState, (draft) => {
|
|
195
|
+
passMove.reducer(draft, context);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(nextState.players[player1].mana).toBe(10);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("Runtime Behavior", () => {
|
|
203
|
+
it("should return the definition unchanged", () => {
|
|
204
|
+
const definition = {
|
|
205
|
+
condition: (state: TestGameState) => state.players[player1].mana >= 1,
|
|
206
|
+
reducer: (draft: any) => {
|
|
207
|
+
draft.players[player1].mana -= 1;
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const result = createMove<TestGameState, TestMoveParams, "quest">(
|
|
212
|
+
definition,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Runtime behavior: returns the same object
|
|
216
|
+
expect(result).toBe(definition);
|
|
217
|
+
expect(result.condition).toBe(definition.condition);
|
|
218
|
+
expect(result.reducer).toBe(definition.reducer);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should work with move definitions without conditions", () => {
|
|
222
|
+
const definition = {
|
|
223
|
+
reducer: (draft: any) => {
|
|
224
|
+
draft.players[player1].health -= 1;
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const result = createMove<TestGameState, TestMoveParams, "pass">(
|
|
229
|
+
definition,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(result).toBe(definition);
|
|
233
|
+
expect(result.condition).toBeUndefined();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should work with move definitions with metadata", () => {
|
|
237
|
+
const definition = {
|
|
238
|
+
reducer: (draft: any) => {
|
|
239
|
+
draft.players[player1].health += 5;
|
|
240
|
+
},
|
|
241
|
+
metadata: {
|
|
242
|
+
category: "healing",
|
|
243
|
+
tags: ["buff"],
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const result = createMove<TestGameState, TestMoveParams, "pass">(
|
|
248
|
+
definition,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(result.metadata?.category).toBe("healing");
|
|
252
|
+
expect(result.metadata?.tags).toEqual(["buff"]);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("Integration with GameMoveDefinitions", () => {
|
|
257
|
+
it("should work correctly when aggregated into move map", () => {
|
|
258
|
+
// Create individual moves using createMove
|
|
259
|
+
const quest = createMove<TestGameState, TestMoveParams, "quest">({
|
|
260
|
+
reducer: (draft, context) => {
|
|
261
|
+
const { cardId } = context.params;
|
|
262
|
+
draft.cards[cardId].damage += 1;
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const challenge = createMove<TestGameState, TestMoveParams, "challenge">({
|
|
267
|
+
reducer: (draft, context) => {
|
|
268
|
+
const { attackerId, defenderId } = context.params;
|
|
269
|
+
draft.cards[defenderId].damage += 2;
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const pass = createMove<TestGameState, TestMoveParams, "pass">({
|
|
274
|
+
reducer: (draft, context) => {
|
|
275
|
+
draft.players[context.playerId].mana = 10;
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Aggregate into a move map (simulating GameMoveDefinitions)
|
|
280
|
+
const moves = {
|
|
281
|
+
quest,
|
|
282
|
+
challenge,
|
|
283
|
+
pass,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Verify all moves are accessible and work correctly
|
|
287
|
+
expect(moves.quest).toBe(quest);
|
|
288
|
+
expect(moves.challenge).toBe(challenge);
|
|
289
|
+
expect(moves.pass).toBe(pass);
|
|
290
|
+
|
|
291
|
+
// Test executing quest move
|
|
292
|
+
const questContext: MoveContext<TestMoveParams["quest"]> =
|
|
293
|
+
createMockContext({
|
|
294
|
+
playerId: player1,
|
|
295
|
+
params: { cardId: card1 },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const state1 = produce(initialState, (draft) => {
|
|
299
|
+
moves.quest.reducer(draft, questContext);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(state1.cards[card1].damage).toBe(1);
|
|
303
|
+
|
|
304
|
+
// Test executing challenge move
|
|
305
|
+
const challengeContext: MoveContext<TestMoveParams["challenge"]> =
|
|
306
|
+
createMockContext({
|
|
307
|
+
playerId: player1,
|
|
308
|
+
params: { attackerId: card1, defenderId: card2 },
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const state2 = produce(state1, (draft) => {
|
|
312
|
+
moves.challenge.reducer(draft, challengeContext);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(state2.cards[card2].damage).toBe(2);
|
|
316
|
+
|
|
317
|
+
// Test executing pass move
|
|
318
|
+
const passContext: MoveContext<TestMoveParams["pass"]> =
|
|
319
|
+
createMockContext({
|
|
320
|
+
playerId: player1,
|
|
321
|
+
params: {},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const state3 = produce(state2, (draft) => {
|
|
325
|
+
moves.pass.reducer(draft, passContext);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(state3.players[player1].mana).toBe(10);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { GameMoveDefinition } from "../game-definition/move-definitions";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a type-safe move definition with proper parameter narrowing
|
|
5
|
+
*
|
|
6
|
+
* This helper ensures that move parameters are correctly narrowed to the specific
|
|
7
|
+
* move's parameter type, avoiding TypeScript's limitations with module boundary
|
|
8
|
+
* type inference.
|
|
9
|
+
*
|
|
10
|
+
* @template TState - Game state type
|
|
11
|
+
* @template TMoves - Record of move names to parameter types
|
|
12
|
+
* @template K - Specific move name (keyof TMoves)
|
|
13
|
+
* @template TCardMeta - Card metadata type
|
|
14
|
+
* @template TCardDefinition - Card definition type
|
|
15
|
+
*
|
|
16
|
+
* @param definition - The move definition (reducer, condition, metadata)
|
|
17
|
+
* @returns The same move definition with proper type narrowing
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* type GameMoves = {
|
|
22
|
+
* quest: { cardId: string };
|
|
23
|
+
* playCard: { cardId: string; cost: number };
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* export const quest = createMove<GameState, GameMoves, "quest", CardMeta>({
|
|
27
|
+
* condition: (state, context) => {
|
|
28
|
+
* const { cardId } = context.params; // ✅ Typed as { cardId: string }
|
|
29
|
+
* return true;
|
|
30
|
+
* },
|
|
31
|
+
* reducer: (draft, context) => {
|
|
32
|
+
* const { cardId } = context.params; // ✅ Typed as { cardId: string }
|
|
33
|
+
* // Implementation...
|
|
34
|
+
* }
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* **Why this is needed**:
|
|
39
|
+
* Without this helper, when you export a move with an explicit type annotation:
|
|
40
|
+
* ```typescript
|
|
41
|
+
* export const quest: MoveDefinition<GameState, GameMoves, CardMeta> = {...}
|
|
42
|
+
* ```
|
|
43
|
+
* TypeScript sees `context.params` as the full `GameMoves` type union, not just
|
|
44
|
+
* the narrowed `GameMoves["quest"]` type.
|
|
45
|
+
*
|
|
46
|
+
* This helper uses TypeScript's generic inference to properly narrow the type
|
|
47
|
+
* at the definition site, avoiding module boundary issues.
|
|
48
|
+
*/
|
|
49
|
+
export function createMove<
|
|
50
|
+
TState,
|
|
51
|
+
TMoves extends Record<string, any>,
|
|
52
|
+
K extends keyof TMoves,
|
|
53
|
+
TCardMeta = any,
|
|
54
|
+
TCardDefinition = any,
|
|
55
|
+
>(
|
|
56
|
+
definition: GameMoveDefinition<
|
|
57
|
+
TState,
|
|
58
|
+
TMoves[K], // ✅ Narrow to specific move's params
|
|
59
|
+
TCardMeta,
|
|
60
|
+
TCardDefinition
|
|
61
|
+
>,
|
|
62
|
+
): GameMoveDefinition<TState, TMoves[K], TCardMeta, TCardDefinition> {
|
|
63
|
+
return definition;
|
|
64
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Move Enumeration System
|
|
3
|
+
*
|
|
4
|
+
* Provides types and interfaces for enumerating all possible moves
|
|
5
|
+
* with their valid parameters. This enables AI agents and UI components
|
|
6
|
+
* to discover available actions at any game state.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CardOperations } from "../operations/card-operations";
|
|
12
|
+
import type { CardRegistry } from "../operations/card-registry";
|
|
13
|
+
import type { CounterOperations } from "../operations/counter-operations";
|
|
14
|
+
import type { GameOperations } from "../operations/game-operations";
|
|
15
|
+
import type { ZoneOperations } from "../operations/zone-operations";
|
|
16
|
+
import type { SeededRNG } from "../rng/seeded-rng";
|
|
17
|
+
import type { CardId, PlayerId } from "../types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Enumerated Move Result
|
|
21
|
+
*
|
|
22
|
+
* Represents a single valid move with all its parameter values.
|
|
23
|
+
* Returned by RuleEngine.enumerateMoves() for each possible move + parameter combination.
|
|
24
|
+
*
|
|
25
|
+
* @template TParams - Move-specific parameter type
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const moves = engine.enumerateMoves(playerId, { validOnly: true });
|
|
30
|
+
* for (const move of moves) {
|
|
31
|
+
* console.log(`${move.moveId}:`, move.params);
|
|
32
|
+
* if (move.isValid) {
|
|
33
|
+
* // Execute the move
|
|
34
|
+
* engine.executeMove(move.moveId, {
|
|
35
|
+
* playerId: move.playerId,
|
|
36
|
+
* params: move.params
|
|
37
|
+
* });
|
|
38
|
+
* }
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export type EnumeratedMove<TParams = unknown> = {
|
|
43
|
+
/** Move identifier */
|
|
44
|
+
moveId: string;
|
|
45
|
+
|
|
46
|
+
/** Player who can execute this move */
|
|
47
|
+
playerId: PlayerId;
|
|
48
|
+
|
|
49
|
+
/** Fully populated parameters for this move */
|
|
50
|
+
params: TParams;
|
|
51
|
+
|
|
52
|
+
/** Optional source card for this move */
|
|
53
|
+
sourceCardId?: CardId;
|
|
54
|
+
|
|
55
|
+
/** Optional targets for this move */
|
|
56
|
+
targets?: string[][];
|
|
57
|
+
|
|
58
|
+
/** Whether this move is currently valid (passed condition check) */
|
|
59
|
+
isValid: boolean;
|
|
60
|
+
|
|
61
|
+
/** If not valid, reason for failure */
|
|
62
|
+
validationError?: {
|
|
63
|
+
reason: string;
|
|
64
|
+
errorCode: string;
|
|
65
|
+
context?: Record<string, unknown>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Optional metadata for UI/AI consumption */
|
|
69
|
+
metadata?: {
|
|
70
|
+
displayName?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
category?: string;
|
|
73
|
+
priority?: number;
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Enumeration Context
|
|
80
|
+
*
|
|
81
|
+
* Provided to enumerator functions, contains all information needed
|
|
82
|
+
* to discover valid parameters. Similar to MoveContext but focused on
|
|
83
|
+
* read-only operations for parameter discovery.
|
|
84
|
+
*
|
|
85
|
+
* @template TCardMeta - Card metadata type
|
|
86
|
+
* @template TCardDefinition - Card definition type
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const enumerator: MoveEnumerator<GameState, PlayCardParams> = (state, context) => {
|
|
91
|
+
* // Get all cards in player's hand
|
|
92
|
+
* const handCards = context.zones.getCardsInZone('hand', context.playerId);
|
|
93
|
+
*
|
|
94
|
+
* // Generate parameter combination for each card
|
|
95
|
+
* return handCards.map(cardId => ({ cardId }));
|
|
96
|
+
* };
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export type MoveEnumerationContext<
|
|
100
|
+
TCardMeta = unknown,
|
|
101
|
+
TCardDefinition = unknown,
|
|
102
|
+
> = {
|
|
103
|
+
/** Player to enumerate moves for */
|
|
104
|
+
playerId: PlayerId;
|
|
105
|
+
|
|
106
|
+
/** Zone operations for querying card locations */
|
|
107
|
+
zones: ZoneOperations;
|
|
108
|
+
|
|
109
|
+
/** Card operations for querying card state */
|
|
110
|
+
cards: CardOperations<TCardMeta>;
|
|
111
|
+
|
|
112
|
+
/** Game operations for game-level state */
|
|
113
|
+
game: GameOperations;
|
|
114
|
+
|
|
115
|
+
/** Counter operations for querying card counters/flags */
|
|
116
|
+
counters: CounterOperations;
|
|
117
|
+
|
|
118
|
+
/** Card registry for static card definitions */
|
|
119
|
+
registry?: CardRegistry<TCardDefinition>;
|
|
120
|
+
|
|
121
|
+
/** Flow state (turn, phase, segment) */
|
|
122
|
+
flow?: {
|
|
123
|
+
currentPhase?: string;
|
|
124
|
+
currentSegment?: string;
|
|
125
|
+
turn: number;
|
|
126
|
+
currentPlayer?: PlayerId;
|
|
127
|
+
isFirstTurn: boolean;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** RNG for deterministic enumeration if needed */
|
|
131
|
+
rng: SeededRNG;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Move Enumerator Function
|
|
136
|
+
*
|
|
137
|
+
* Game-provided function that generates all possible parameter combinations
|
|
138
|
+
* for a given move. Returns an array of parameter objects.
|
|
139
|
+
*
|
|
140
|
+
* Each parameter object returned will be validated against the move's condition
|
|
141
|
+
* and included in the enumeration results.
|
|
142
|
+
*
|
|
143
|
+
* @template TGameState - Game state type
|
|
144
|
+
* @template TParams - Move-specific parameter type
|
|
145
|
+
* @template TCardMeta - Card metadata type
|
|
146
|
+
* @template TCardDefinition - Card definition type
|
|
147
|
+
*
|
|
148
|
+
* @param state - Current game state (readonly)
|
|
149
|
+
* @param context - Enumeration context with player, operations, etc.
|
|
150
|
+
* @returns Array of possible parameter combinations
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* // Simple card play enumerator
|
|
155
|
+
* const playCardEnumerator: MoveEnumerator<GameState, PlayCardParams> = (state, context) => {
|
|
156
|
+
* const handCards = context.zones.getCardsInZone('hand', context.playerId);
|
|
157
|
+
* return handCards.map(cardId => ({ cardId }));
|
|
158
|
+
* };
|
|
159
|
+
*
|
|
160
|
+
* // Attack enumerator with multiple targets
|
|
161
|
+
* const attackEnumerator: MoveEnumerator<GameState, AttackParams> = (state, context) => {
|
|
162
|
+
* const results: AttackParams[] = [];
|
|
163
|
+
* const attackers = context.zones.getCardsInZone('field', context.playerId);
|
|
164
|
+
*
|
|
165
|
+
* for (const attackerId of attackers) {
|
|
166
|
+
* const targets = getValidTargets(state, attackerId);
|
|
167
|
+
* for (const targetId of targets) {
|
|
168
|
+
* results.push({ attackerId, targetId });
|
|
169
|
+
* }
|
|
170
|
+
* }
|
|
171
|
+
*
|
|
172
|
+
* return results;
|
|
173
|
+
* };
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export type MoveEnumerator<
|
|
177
|
+
TGameState,
|
|
178
|
+
TParams = unknown,
|
|
179
|
+
TCardMeta = unknown,
|
|
180
|
+
TCardDefinition = unknown,
|
|
181
|
+
> = (
|
|
182
|
+
state: TGameState,
|
|
183
|
+
context: MoveEnumerationContext<TCardMeta, TCardDefinition>,
|
|
184
|
+
) => TParams[];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Move Enumeration Options
|
|
188
|
+
*
|
|
189
|
+
* Configuration for enumeration behavior.
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* // Get only valid moves
|
|
194
|
+
* const validMoves = engine.enumerateMoves(playerId, {
|
|
195
|
+
* validOnly: true
|
|
196
|
+
* });
|
|
197
|
+
*
|
|
198
|
+
* // Get all moves with metadata
|
|
199
|
+
* const allMoves = engine.enumerateMoves(playerId, {
|
|
200
|
+
* validOnly: false,
|
|
201
|
+
* includeMetadata: true
|
|
202
|
+
* });
|
|
203
|
+
*
|
|
204
|
+
* // Enumerate specific moves only
|
|
205
|
+
* const attackMoves = engine.enumerateMoves(playerId, {
|
|
206
|
+
* moveIds: ['attack', 'special-attack'],
|
|
207
|
+
* validOnly: true
|
|
208
|
+
* });
|
|
209
|
+
*
|
|
210
|
+
* // Limit results per move
|
|
211
|
+
* const limitedMoves = engine.enumerateMoves(playerId, {
|
|
212
|
+
* maxPerMove: 10 // Max 10 parameter combinations per move
|
|
213
|
+
* });
|
|
214
|
+
* ```
|
|
215
|
+
*/
|
|
216
|
+
export type MoveEnumerationOptions = {
|
|
217
|
+
/** Only return valid moves (passed condition check). Default: false */
|
|
218
|
+
validOnly?: boolean;
|
|
219
|
+
|
|
220
|
+
/** Include metadata in results. Default: false */
|
|
221
|
+
includeMetadata?: boolean;
|
|
222
|
+
|
|
223
|
+
/** Filter to specific move IDs. Default: all moves */
|
|
224
|
+
moveIds?: string[];
|
|
225
|
+
|
|
226
|
+
/** Maximum number of results per move (optional limit) */
|
|
227
|
+
maxPerMove?: number;
|
|
228
|
+
};
|