@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,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Target Resolution - Interfaces for resolving DSL to actual card selections
|
|
3
|
+
*
|
|
4
|
+
* This module defines the contracts that game engines implement to
|
|
5
|
+
* resolve target DSL specifications into actual card selections.
|
|
6
|
+
*
|
|
7
|
+
* @module targeting/target-resolver
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CardInstance } from "../cards/card-instance";
|
|
11
|
+
import type { PlayerId } from "../types";
|
|
12
|
+
import type {
|
|
13
|
+
BaseContext,
|
|
14
|
+
TargetCount,
|
|
15
|
+
TargetDSL,
|
|
16
|
+
TargetingUIHint,
|
|
17
|
+
} from "./target-dsl";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Resolution Context
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Context provided during target resolution
|
|
25
|
+
*
|
|
26
|
+
* Contains all information needed to evaluate targeting constraints.
|
|
27
|
+
* Game engines extend this with game-specific context.
|
|
28
|
+
*
|
|
29
|
+
* @typeParam TGameState - The game state type
|
|
30
|
+
* @typeParam TCard - The card instance type
|
|
31
|
+
*/
|
|
32
|
+
export interface TargetResolutionContext<
|
|
33
|
+
TGameState,
|
|
34
|
+
TCard extends CardInstance<unknown>,
|
|
35
|
+
> {
|
|
36
|
+
/** Current game state */
|
|
37
|
+
state: TGameState;
|
|
38
|
+
|
|
39
|
+
/** The card that is the source of targeting (if any) */
|
|
40
|
+
sourceCard?: TCard;
|
|
41
|
+
|
|
42
|
+
/** The player performing the targeting action */
|
|
43
|
+
sourcePlayer: PlayerId;
|
|
44
|
+
|
|
45
|
+
/** Previously selected targets (for multi-target validation) */
|
|
46
|
+
previousTargets?: TCard[];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Game-specific context extensions
|
|
50
|
+
* Games add properties like triggerSource, attacker, defender, etc.
|
|
51
|
+
*/
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Target Resolver Interface
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Interface for target resolution implementations
|
|
61
|
+
*
|
|
62
|
+
* Game engines implement this to provide targeting logic specific to their rules.
|
|
63
|
+
*
|
|
64
|
+
* @typeParam TGameState - The game state type
|
|
65
|
+
* @typeParam TCard - The card instance type
|
|
66
|
+
* @typeParam TTarget - The target DSL type (game-specific extension)
|
|
67
|
+
*/
|
|
68
|
+
export interface TargetResolver<
|
|
69
|
+
TGameState,
|
|
70
|
+
TCard extends CardInstance<unknown>,
|
|
71
|
+
TTarget extends TargetDSL,
|
|
72
|
+
> {
|
|
73
|
+
/**
|
|
74
|
+
* Get all valid targets matching the DSL specification
|
|
75
|
+
*
|
|
76
|
+
* @param target - Target DSL specification
|
|
77
|
+
* @param context - Resolution context
|
|
78
|
+
* @returns Array of cards that are valid targets
|
|
79
|
+
*/
|
|
80
|
+
getValidTargets(
|
|
81
|
+
target: TTarget,
|
|
82
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
83
|
+
): TCard[];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a specific card is a valid target
|
|
87
|
+
*
|
|
88
|
+
* @param card - Card to check
|
|
89
|
+
* @param target - Target DSL specification
|
|
90
|
+
* @param context - Resolution context
|
|
91
|
+
* @returns true if the card is a valid target
|
|
92
|
+
*/
|
|
93
|
+
isValidTarget(
|
|
94
|
+
card: TCard,
|
|
95
|
+
target: TTarget,
|
|
96
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
97
|
+
): boolean;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate a target selection
|
|
101
|
+
*
|
|
102
|
+
* @param selectedTargets - Cards selected by the player
|
|
103
|
+
* @param target - Target DSL specification
|
|
104
|
+
* @param context - Resolution context
|
|
105
|
+
* @returns Validation result with error message if invalid
|
|
106
|
+
*/
|
|
107
|
+
validateSelection(
|
|
108
|
+
selectedTargets: TCard[],
|
|
109
|
+
target: TTarget,
|
|
110
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
111
|
+
): TargetValidationResult;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get UI hints for target selection interface
|
|
115
|
+
*
|
|
116
|
+
* @param target - Target DSL specification
|
|
117
|
+
* @param context - Resolution context
|
|
118
|
+
* @returns UI hints for rendering selection interface
|
|
119
|
+
*/
|
|
120
|
+
getTargetingUI(
|
|
121
|
+
target: TTarget,
|
|
122
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
123
|
+
): TargetingUIHint;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// Validation Result
|
|
128
|
+
// ============================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Result of target selection validation
|
|
132
|
+
*/
|
|
133
|
+
export interface TargetValidationResult {
|
|
134
|
+
/** Whether the selection is valid */
|
|
135
|
+
valid: boolean;
|
|
136
|
+
|
|
137
|
+
/** Error message if invalid */
|
|
138
|
+
error?: string;
|
|
139
|
+
|
|
140
|
+
/** Specific issues with individual targets */
|
|
141
|
+
issues?: TargetIssue[];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Issue with a specific target in a selection
|
|
146
|
+
*/
|
|
147
|
+
export interface TargetIssue {
|
|
148
|
+
/** Index of the problematic target */
|
|
149
|
+
index: number;
|
|
150
|
+
|
|
151
|
+
/** ID of the problematic card */
|
|
152
|
+
cardId: string;
|
|
153
|
+
|
|
154
|
+
/** Description of the issue */
|
|
155
|
+
reason: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// Helper Functions
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a successful validation result
|
|
164
|
+
*/
|
|
165
|
+
export function validSelection(): TargetValidationResult {
|
|
166
|
+
return { valid: true };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create a failed validation result
|
|
171
|
+
*/
|
|
172
|
+
export function invalidSelection(
|
|
173
|
+
error: string,
|
|
174
|
+
issues?: TargetIssue[],
|
|
175
|
+
): TargetValidationResult {
|
|
176
|
+
return { valid: false, error, issues };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Validate target count against a specification
|
|
181
|
+
*/
|
|
182
|
+
export function validateTargetCount(
|
|
183
|
+
selectedCount: number,
|
|
184
|
+
targetCount: TargetCount | undefined,
|
|
185
|
+
availableCount: number,
|
|
186
|
+
): TargetValidationResult {
|
|
187
|
+
// Default to exactly 1 if not specified
|
|
188
|
+
const count = targetCount ?? 1;
|
|
189
|
+
|
|
190
|
+
if (count === "all") {
|
|
191
|
+
// "all" is valid as long as we selected all available
|
|
192
|
+
// (or the player selected what they could)
|
|
193
|
+
return validSelection();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof count === "number") {
|
|
197
|
+
if (selectedCount !== count) {
|
|
198
|
+
// Check if there weren't enough available
|
|
199
|
+
if (availableCount < count) {
|
|
200
|
+
return invalidSelection(
|
|
201
|
+
`Expected ${count} target(s) but only ${availableCount} available`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return invalidSelection(
|
|
205
|
+
`Expected exactly ${count} target(s), but got ${selectedCount}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return validSelection();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if ("exactly" in count) {
|
|
212
|
+
if (selectedCount !== count.exactly) {
|
|
213
|
+
return invalidSelection(
|
|
214
|
+
`Expected exactly ${count.exactly} target(s), but got ${selectedCount}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return validSelection();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if ("upTo" in count) {
|
|
221
|
+
if (selectedCount > count.upTo) {
|
|
222
|
+
return invalidSelection(
|
|
223
|
+
`Expected at most ${count.upTo} target(s), but got ${selectedCount}`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return validSelection();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if ("atLeast" in count) {
|
|
230
|
+
if (selectedCount < count.atLeast) {
|
|
231
|
+
return invalidSelection(
|
|
232
|
+
`Expected at least ${count.atLeast} target(s), but got ${selectedCount}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return validSelection();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if ("between" in count) {
|
|
239
|
+
const [min, max] = count.between;
|
|
240
|
+
if (selectedCount < min) {
|
|
241
|
+
return invalidSelection(
|
|
242
|
+
`Expected at least ${min} target(s), but got ${selectedCount}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
if (selectedCount > max) {
|
|
246
|
+
return invalidSelection(
|
|
247
|
+
`Expected at most ${max} target(s), but got ${selectedCount}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return validSelection();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return validSelection();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ============================================================================
|
|
257
|
+
// Abstract Base Resolver
|
|
258
|
+
// ============================================================================
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Abstract base class for target resolvers
|
|
262
|
+
*
|
|
263
|
+
* Provides common functionality that game-specific resolvers can extend.
|
|
264
|
+
*
|
|
265
|
+
* @typeParam TGameState - The game state type
|
|
266
|
+
* @typeParam TCard - The card instance type
|
|
267
|
+
* @typeParam TTarget - The target DSL type
|
|
268
|
+
* @typeParam TContext - The context type
|
|
269
|
+
*/
|
|
270
|
+
export abstract class BaseTargetResolver<
|
|
271
|
+
TGameState,
|
|
272
|
+
TCard extends CardInstance<unknown>,
|
|
273
|
+
TTarget extends TargetDSL<any, any>,
|
|
274
|
+
TContext extends BaseContext = BaseContext,
|
|
275
|
+
> implements TargetResolver<TGameState, TCard, TTarget>
|
|
276
|
+
{
|
|
277
|
+
/**
|
|
278
|
+
* Get all cards in specified zones
|
|
279
|
+
* Game engines override this to access their zone system
|
|
280
|
+
*/
|
|
281
|
+
protected abstract getCardsInZones(
|
|
282
|
+
zones: string[] | undefined,
|
|
283
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
284
|
+
): TCard[];
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Check if a card matches ownership constraints
|
|
288
|
+
*/
|
|
289
|
+
protected abstract matchesOwnership(
|
|
290
|
+
card: TCard,
|
|
291
|
+
owner: "you" | "opponent" | "any" | undefined,
|
|
292
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
293
|
+
): boolean;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if a card matches card type constraints
|
|
297
|
+
*/
|
|
298
|
+
protected abstract matchesCardType(
|
|
299
|
+
card: TCard,
|
|
300
|
+
cardTypes: string[] | undefined,
|
|
301
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
302
|
+
): boolean;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Apply game-specific filters to a card
|
|
306
|
+
*/
|
|
307
|
+
protected abstract applyFilter(
|
|
308
|
+
card: TCard,
|
|
309
|
+
filter: unknown,
|
|
310
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
311
|
+
): boolean;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Generate human-readable description of the target
|
|
315
|
+
*/
|
|
316
|
+
protected abstract generateDescription(target: TTarget): string;
|
|
317
|
+
|
|
318
|
+
getValidTargets(
|
|
319
|
+
target: TTarget,
|
|
320
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
321
|
+
): TCard[] {
|
|
322
|
+
// Handle self selector
|
|
323
|
+
if (target.selector === "self" && context.sourceCard) {
|
|
324
|
+
return [context.sourceCard];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Get all cards in specified zones
|
|
328
|
+
let candidates = this.getCardsInZones(target.zones, context);
|
|
329
|
+
|
|
330
|
+
// Apply ownership filter
|
|
331
|
+
candidates = candidates.filter((card) =>
|
|
332
|
+
this.matchesOwnership(card, target.owner, context),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
// Apply card type filter
|
|
336
|
+
candidates = candidates.filter((card) =>
|
|
337
|
+
this.matchesCardType(card, target.cardTypes, context),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Apply game-specific filter
|
|
341
|
+
if (target.filter) {
|
|
342
|
+
candidates = candidates.filter((card) =>
|
|
343
|
+
this.applyFilter(card, target.filter, context),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Handle excludeSelf
|
|
348
|
+
if (target.excludeSelf && context.sourceCard) {
|
|
349
|
+
candidates = candidates.filter(
|
|
350
|
+
(card) => card.id !== context.sourceCard!.id,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Handle different targets requirement
|
|
355
|
+
if (target.requireDifferentTargets && context.previousTargets) {
|
|
356
|
+
const previousIds = new Set(context.previousTargets.map((c) => c.id));
|
|
357
|
+
candidates = candidates.filter((card) => !previousIds.has(card.id));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return candidates;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
isValidTarget(
|
|
364
|
+
card: TCard,
|
|
365
|
+
target: TTarget,
|
|
366
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
367
|
+
): boolean {
|
|
368
|
+
const validTargets = this.getValidTargets(target, context);
|
|
369
|
+
return validTargets.some((t) => t.id === card.id);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
validateSelection(
|
|
373
|
+
selectedTargets: TCard[],
|
|
374
|
+
target: TTarget,
|
|
375
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
376
|
+
): TargetValidationResult {
|
|
377
|
+
const validTargets = this.getValidTargets(target, context);
|
|
378
|
+
|
|
379
|
+
// Validate count
|
|
380
|
+
const countResult = validateTargetCount(
|
|
381
|
+
selectedTargets.length,
|
|
382
|
+
target.count,
|
|
383
|
+
validTargets.length,
|
|
384
|
+
);
|
|
385
|
+
if (!countResult.valid) {
|
|
386
|
+
return countResult;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Validate each selected target is valid
|
|
390
|
+
const issues: TargetIssue[] = [];
|
|
391
|
+
for (let i = 0; i < selectedTargets.length; i++) {
|
|
392
|
+
const selected = selectedTargets[i];
|
|
393
|
+
if (!selected) {
|
|
394
|
+
issues.push({
|
|
395
|
+
index: i,
|
|
396
|
+
cardId: "undefined",
|
|
397
|
+
reason: "Target is undefined",
|
|
398
|
+
});
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!validTargets.some((t) => t.id === selected.id)) {
|
|
403
|
+
issues.push({
|
|
404
|
+
index: i,
|
|
405
|
+
cardId: String(selected.id),
|
|
406
|
+
reason: "Not a valid target",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (issues.length > 0) {
|
|
412
|
+
return invalidSelection("Some selected targets are invalid", issues);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check for duplicates if required
|
|
416
|
+
if (target.requireDifferentTargets) {
|
|
417
|
+
const ids = selectedTargets.map((t) => t.id);
|
|
418
|
+
const uniqueIds = new Set(ids);
|
|
419
|
+
if (uniqueIds.size !== ids.length) {
|
|
420
|
+
return invalidSelection("All targets must be different cards");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return validSelection();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
getTargetingUI(
|
|
428
|
+
target: TTarget,
|
|
429
|
+
context: TargetResolutionContext<TGameState, TCard>,
|
|
430
|
+
): TargetingUIHint {
|
|
431
|
+
const validTargets = this.getValidTargets(target, context);
|
|
432
|
+
|
|
433
|
+
// Determine selection type
|
|
434
|
+
let selectionType: TargetingUIHint["selectionType"];
|
|
435
|
+
if (target.selector === "self") {
|
|
436
|
+
selectionType = "none";
|
|
437
|
+
} else if (target.selector === "all" || target.selector === "each") {
|
|
438
|
+
selectionType = "automatic";
|
|
439
|
+
} else if (target.selector === "chosen") {
|
|
440
|
+
const maxCount =
|
|
441
|
+
target.count === "all"
|
|
442
|
+
? validTargets.length
|
|
443
|
+
: typeof target.count === "number"
|
|
444
|
+
? target.count
|
|
445
|
+
: target.count && "upTo" in target.count
|
|
446
|
+
? target.count.upTo
|
|
447
|
+
: 1;
|
|
448
|
+
selectionType = maxCount > 1 ? "multiple" : "single";
|
|
449
|
+
} else {
|
|
450
|
+
selectionType = "single";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Calculate min/max
|
|
454
|
+
let minSelections = 1;
|
|
455
|
+
let maxSelections: number | "unlimited" = 1;
|
|
456
|
+
|
|
457
|
+
if (target.count === "all") {
|
|
458
|
+
minSelections = validTargets.length;
|
|
459
|
+
maxSelections = validTargets.length;
|
|
460
|
+
} else if (typeof target.count === "number") {
|
|
461
|
+
minSelections = target.count;
|
|
462
|
+
maxSelections = target.count;
|
|
463
|
+
} else if (target.count && "upTo" in target.count) {
|
|
464
|
+
minSelections = 0;
|
|
465
|
+
maxSelections = target.count.upTo;
|
|
466
|
+
} else if (target.count && "atLeast" in target.count) {
|
|
467
|
+
minSelections = target.count.atLeast;
|
|
468
|
+
maxSelections = "unlimited";
|
|
469
|
+
} else if (target.count && "between" in target.count) {
|
|
470
|
+
minSelections = target.count.between[0];
|
|
471
|
+
maxSelections = target.count.between[1];
|
|
472
|
+
} else if (target.count && "exactly" in target.count) {
|
|
473
|
+
minSelections = target.count.exactly;
|
|
474
|
+
maxSelections = target.count.exactly;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
selectionType,
|
|
479
|
+
minSelections,
|
|
480
|
+
maxSelections,
|
|
481
|
+
prompt: this.generateDescription(target),
|
|
482
|
+
optional: minSelections === 0,
|
|
483
|
+
highlightZones: target.zones || [],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|