@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,273 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createPlayerId, createZoneId } from "../types";
|
|
3
|
+
import type { TargetDefinition, TargetRestriction } from "./target-definition";
|
|
4
|
+
|
|
5
|
+
describe("Target Definition Types", () => {
|
|
6
|
+
describe("TargetDefinition", () => {
|
|
7
|
+
it("should define target with filter and count", () => {
|
|
8
|
+
const target: TargetDefinition = {
|
|
9
|
+
filter: { zone: createZoneId("play") },
|
|
10
|
+
count: 1,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(target.filter).toBeDefined();
|
|
14
|
+
expect(target.count).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should support optional targets with min/max count", () => {
|
|
18
|
+
const target: TargetDefinition = {
|
|
19
|
+
filter: { type: "creature" },
|
|
20
|
+
count: { min: 0, max: 3 },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
expect(target.count).toEqual({ min: 0, max: 3 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should support required targets with exact count", () => {
|
|
27
|
+
const target: TargetDefinition = {
|
|
28
|
+
filter: { zone: createZoneId("hand") },
|
|
29
|
+
count: 2,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
expect(target.count).toBe(2);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should support targeting restrictions", () => {
|
|
36
|
+
const target: TargetDefinition = {
|
|
37
|
+
filter: { type: "creature" },
|
|
38
|
+
count: 1,
|
|
39
|
+
restrictions: ["not-self"],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
expect(target.restrictions).toContain("not-self");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should support multiple restrictions", () => {
|
|
46
|
+
const target: TargetDefinition = {
|
|
47
|
+
filter: { zone: createZoneId("play") },
|
|
48
|
+
count: 2,
|
|
49
|
+
restrictions: ["not-self", "different-targets"],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
expect(target.restrictions).toHaveLength(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should work without restrictions", () => {
|
|
56
|
+
const target: TargetDefinition = {
|
|
57
|
+
filter: { type: "land" },
|
|
58
|
+
count: 1,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
expect(target.restrictions).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("TargetRestriction", () => {
|
|
66
|
+
it("should support 'not-self' restriction", () => {
|
|
67
|
+
const restriction: TargetRestriction = "not-self";
|
|
68
|
+
expect(restriction).toBe("not-self");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should support 'not-controller' restriction", () => {
|
|
72
|
+
const restriction: TargetRestriction = "not-controller";
|
|
73
|
+
expect(restriction).toBe("not-controller");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should support 'not-owner' restriction", () => {
|
|
77
|
+
const restriction: TargetRestriction = "not-owner";
|
|
78
|
+
expect(restriction).toBe("not-owner");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should support 'different-targets' restriction", () => {
|
|
82
|
+
const restriction: TargetRestriction = "different-targets";
|
|
83
|
+
expect(restriction).toBe("different-targets");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("Target Count Types", () => {
|
|
88
|
+
it("should support exact count as number", () => {
|
|
89
|
+
const count: number | { min: number; max: number } = 1;
|
|
90
|
+
expect(count).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should support range with min and max", () => {
|
|
94
|
+
const count: number | { min: number; max: number } = {
|
|
95
|
+
min: 1,
|
|
96
|
+
max: 3,
|
|
97
|
+
};
|
|
98
|
+
expect(count).toEqual({ min: 1, max: 3 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should support optional targets with min 0", () => {
|
|
102
|
+
const count: number | { min: number; max: number } = {
|
|
103
|
+
min: 0,
|
|
104
|
+
max: 5,
|
|
105
|
+
};
|
|
106
|
+
expect(count.min).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should support unbounded max targets", () => {
|
|
110
|
+
const count: number | { min: number; max: number } = {
|
|
111
|
+
min: 1,
|
|
112
|
+
max: Number.POSITIVE_INFINITY,
|
|
113
|
+
};
|
|
114
|
+
expect(count.max).toBe(Number.POSITIVE_INFINITY);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("Multi-Target Definitions", () => {
|
|
119
|
+
it("should support multiple target groups", () => {
|
|
120
|
+
const targets: TargetDefinition[] = [
|
|
121
|
+
{
|
|
122
|
+
filter: { type: "creature" },
|
|
123
|
+
count: 1,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
filter: { type: "player" },
|
|
127
|
+
count: 1,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
expect(targets).toHaveLength(2);
|
|
132
|
+
expect(targets[0].filter.type).toBe("creature");
|
|
133
|
+
expect(targets[1].filter.type).toBe("player");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should support optional and required targets together", () => {
|
|
137
|
+
const targets: TargetDefinition[] = [
|
|
138
|
+
{
|
|
139
|
+
filter: { type: "creature" },
|
|
140
|
+
count: 1,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
filter: { type: "land" },
|
|
144
|
+
count: { min: 0, max: 2 },
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
expect(typeof targets[0].count).toBe("number");
|
|
149
|
+
expect(typeof targets[1].count).toBe("object");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("Complex Target Definitions", () => {
|
|
154
|
+
it("should support targets with complex filters", () => {
|
|
155
|
+
const playZone = createZoneId("play");
|
|
156
|
+
const target: TargetDefinition = {
|
|
157
|
+
filter: {
|
|
158
|
+
zone: playZone,
|
|
159
|
+
type: "creature",
|
|
160
|
+
properties: {
|
|
161
|
+
basePower: { gte: 3 },
|
|
162
|
+
},
|
|
163
|
+
tapped: false,
|
|
164
|
+
},
|
|
165
|
+
count: { min: 1, max: 3 },
|
|
166
|
+
restrictions: ["not-self"],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
expect(target.filter.zone).toBe(playZone);
|
|
170
|
+
expect(target.filter.type).toBe("creature");
|
|
171
|
+
expect(target.filter.properties?.basePower).toEqual({ gte: 3 });
|
|
172
|
+
expect(target.filter.tapped).toBe(false);
|
|
173
|
+
expect(target.count).toEqual({ min: 1, max: 3 });
|
|
174
|
+
expect(target.restrictions).toContain("not-self");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("should support targets with composite filters", () => {
|
|
178
|
+
const target: TargetDefinition = {
|
|
179
|
+
filter: {
|
|
180
|
+
or: [{ type: "creature" }, { type: "artifact" }],
|
|
181
|
+
},
|
|
182
|
+
count: 2,
|
|
183
|
+
restrictions: ["different-targets"],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
expect(target.filter.or).toBeDefined();
|
|
187
|
+
expect(target.filter.or).toHaveLength(2);
|
|
188
|
+
expect(target.restrictions).toContain("different-targets");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("Real-World Examples", () => {
|
|
193
|
+
it("should model Lightning Bolt (1 target, any creature or player)", () => {
|
|
194
|
+
const target: TargetDefinition = {
|
|
195
|
+
filter: {
|
|
196
|
+
or: [{ type: "creature" }, { type: "player" }],
|
|
197
|
+
},
|
|
198
|
+
count: 1,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
expect(target.count).toBe(1);
|
|
202
|
+
expect(target.filter.or).toHaveLength(2);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should model Wrath of God (no targets)", () => {
|
|
206
|
+
const targets: TargetDefinition[] = [];
|
|
207
|
+
|
|
208
|
+
expect(targets).toHaveLength(0);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should model Fireball (1 required target, additional optional)", () => {
|
|
212
|
+
const targets: TargetDefinition[] = [
|
|
213
|
+
{
|
|
214
|
+
filter: {
|
|
215
|
+
or: [{ type: "creature" }, { type: "player" }],
|
|
216
|
+
},
|
|
217
|
+
count: 1,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
filter: {
|
|
221
|
+
or: [{ type: "creature" }, { type: "player" }],
|
|
222
|
+
},
|
|
223
|
+
count: { min: 0, max: Number.POSITIVE_INFINITY },
|
|
224
|
+
restrictions: ["different-targets"],
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
expect(targets).toHaveLength(2);
|
|
229
|
+
expect(targets[0].count).toBe(1);
|
|
230
|
+
expect(targets[1].count).toEqual({
|
|
231
|
+
min: 0,
|
|
232
|
+
max: Number.POSITIVE_INFINITY,
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should model Pump spell (target creature you control)", () => {
|
|
237
|
+
const player = createPlayerId("player-1");
|
|
238
|
+
const target: TargetDefinition = {
|
|
239
|
+
filter: {
|
|
240
|
+
type: "creature",
|
|
241
|
+
controller: player,
|
|
242
|
+
},
|
|
243
|
+
count: 1,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
expect(target.filter.controller).toBe(player);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should model Fight spell (2 creatures, can't target same)", () => {
|
|
250
|
+
const target: TargetDefinition = {
|
|
251
|
+
filter: { type: "creature" },
|
|
252
|
+
count: 2,
|
|
253
|
+
restrictions: ["different-targets"],
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
expect(target.count).toBe(2);
|
|
257
|
+
expect(target.restrictions).toContain("different-targets");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("should model Act of Treason (opponent's creature)", () => {
|
|
261
|
+
const _player = createPlayerId("player-1");
|
|
262
|
+
const target: TargetDefinition = {
|
|
263
|
+
filter: {
|
|
264
|
+
type: "creature",
|
|
265
|
+
},
|
|
266
|
+
count: 1,
|
|
267
|
+
restrictions: ["not-controller"],
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
expect(target.restrictions).toContain("not-controller");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CardFilter } from "../filtering/card-filter";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Target restriction types
|
|
5
|
+
* Defines additional constraints on target selection beyond the filter
|
|
6
|
+
*/
|
|
7
|
+
export type TargetRestriction =
|
|
8
|
+
| "not-self" // Cannot target the source card itself
|
|
9
|
+
| "not-controller" // Cannot target cards controlled by the move's controller
|
|
10
|
+
| "not-owner" // Cannot target cards owned by the move's player
|
|
11
|
+
| "different-targets"; // All selected targets must be different cards
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Target count specification
|
|
15
|
+
* Can be an exact number or a range (min, max)
|
|
16
|
+
*/
|
|
17
|
+
export type TargetCount =
|
|
18
|
+
| number // Exact count (required)
|
|
19
|
+
| {
|
|
20
|
+
min: number; // Minimum number of targets (0 = optional)
|
|
21
|
+
max: number; // Maximum number of targets (can be Infinity)
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Target definition for a move
|
|
26
|
+
* Specifies what can be targeted and how many targets are needed
|
|
27
|
+
*/
|
|
28
|
+
export type TargetDefinition = {
|
|
29
|
+
/** Filter defining valid target cards/entities */
|
|
30
|
+
filter: CardFilter;
|
|
31
|
+
|
|
32
|
+
/** Number of targets required/allowed */
|
|
33
|
+
count: TargetCount;
|
|
34
|
+
|
|
35
|
+
/** Optional restrictions on target selection */
|
|
36
|
+
restrictions?: TargetRestriction[];
|
|
37
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Target DSL - Core Types
|
|
3
|
+
*
|
|
4
|
+
* Game-agnostic targeting primitives that TCG engines can extend.
|
|
5
|
+
* This module provides the foundational DSL structure for expressing
|
|
6
|
+
* card and player targeting in a declarative, composable way.
|
|
7
|
+
*
|
|
8
|
+
* @module targeting/target-dsl
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CardFilter } from "../filtering/card-filter";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Selector: HOW targets are selected
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Selector scope determines how targets are selected from valid options
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* - "self": Target the source card itself
|
|
22
|
+
* - "chosen": Player actively selects from valid targets
|
|
23
|
+
* - "all": All matching cards are automatically targeted
|
|
24
|
+
* - "each": Semantic alias for "all" (used in effect descriptions)
|
|
25
|
+
* - "any": Single target, typically random or first-match
|
|
26
|
+
* - "random": Explicitly random selection
|
|
27
|
+
*/
|
|
28
|
+
export type SelectorScope =
|
|
29
|
+
| "self" // This card (the source of the ability)
|
|
30
|
+
| "chosen" // Player chooses from valid options
|
|
31
|
+
| "all" // All matching cards
|
|
32
|
+
| "each" // Each matching (semantic alias for effects)
|
|
33
|
+
| "any" // Any single matching card
|
|
34
|
+
| "random"; // Random selection from valid options
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Owner Scope: WHOSE cards can be selected
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Owner scope determines which players' cards are valid targets
|
|
42
|
+
*/
|
|
43
|
+
export type OwnerScope =
|
|
44
|
+
| "you" // Controller of source card
|
|
45
|
+
| "opponent" // Opponent(s)
|
|
46
|
+
| "any"; // Any player's cards
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Target Count: HOW MANY to select
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Target count specification with various semantics
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* - `1`: Exactly 1 target (required)
|
|
57
|
+
* - `{ exactly: 2 }`: Exactly 2 targets
|
|
58
|
+
* - `{ upTo: 3 }`: 0 to 3 targets (player chooses how many)
|
|
59
|
+
* - `{ atLeast: 1 }`: 1 or more targets
|
|
60
|
+
* - `{ between: [2, 4] }`: 2 to 4 targets
|
|
61
|
+
* - `"all"`: All matching targets
|
|
62
|
+
*/
|
|
63
|
+
export type TargetCount =
|
|
64
|
+
| number // Exact count (required)
|
|
65
|
+
| { exactly: number } // Explicit exact count
|
|
66
|
+
| { upTo: number } // 0 to N (optional up to max)
|
|
67
|
+
| { atLeast: number } // N or more (minimum required)
|
|
68
|
+
| { between: [number, number] } // Range [min, max]
|
|
69
|
+
| "all"; // All matching
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Context References: Contextual card references
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Base context for targeting - game engines extend this
|
|
77
|
+
*
|
|
78
|
+
* Provides references to cards based on the current game context,
|
|
79
|
+
* such as the trigger source, combat participants, etc.
|
|
80
|
+
*/
|
|
81
|
+
export interface BaseContext {
|
|
82
|
+
/** Reference the source card itself */
|
|
83
|
+
self?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// Core Target DSL Structure
|
|
88
|
+
// ============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Core Target DSL - The main targeting structure
|
|
92
|
+
*
|
|
93
|
+
* This generic type defines how targets are selected. Game-specific
|
|
94
|
+
* engines extend the filter and context type parameters.
|
|
95
|
+
*
|
|
96
|
+
* @typeParam TFilter - Type of filters (extends CardFilter or game-specific)
|
|
97
|
+
* @typeParam TContext - Type of context references
|
|
98
|
+
*
|
|
99
|
+
* @example Basic character targeting
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const target: TargetDSL = {
|
|
102
|
+
* selector: "chosen",
|
|
103
|
+
* count: 1,
|
|
104
|
+
* owner: "opponent",
|
|
105
|
+
* zones: ["play"],
|
|
106
|
+
* cardTypes: ["character"]
|
|
107
|
+
* };
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @example Targeting with filters
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const target: TargetDSL = {
|
|
113
|
+
* selector: "all",
|
|
114
|
+
* owner: "any",
|
|
115
|
+
* zones: ["play"],
|
|
116
|
+
* filter: { type: "creature", tapped: true }
|
|
117
|
+
* };
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export interface TargetDSL<
|
|
121
|
+
TFilter = CardFilter,
|
|
122
|
+
TContext extends BaseContext = BaseContext,
|
|
123
|
+
> {
|
|
124
|
+
/** How targets are selected (chosen, all, self, etc.) */
|
|
125
|
+
selector: SelectorScope;
|
|
126
|
+
|
|
127
|
+
/** How many targets to select */
|
|
128
|
+
count?: TargetCount;
|
|
129
|
+
|
|
130
|
+
/** Whose cards can be targeted */
|
|
131
|
+
owner?: OwnerScope;
|
|
132
|
+
|
|
133
|
+
/** Which zones to search for targets */
|
|
134
|
+
zones?: string[];
|
|
135
|
+
|
|
136
|
+
/** Card type restriction (game-specific type names) */
|
|
137
|
+
cardTypes?: string[];
|
|
138
|
+
|
|
139
|
+
/** Additional filter criteria (merged with base filter) */
|
|
140
|
+
filter?: TFilter;
|
|
141
|
+
|
|
142
|
+
/** Context references (self, trigger-source, etc.) */
|
|
143
|
+
context?: TContext;
|
|
144
|
+
|
|
145
|
+
/** Exclude the source card from valid targets */
|
|
146
|
+
excludeSelf?: boolean;
|
|
147
|
+
|
|
148
|
+
/** All selected targets must be different cards */
|
|
149
|
+
requireDifferentTargets?: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// UI Hint Types
|
|
154
|
+
// ============================================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* UI hints for target selection interfaces
|
|
158
|
+
*
|
|
159
|
+
* Games can use these to generate appropriate selection UI
|
|
160
|
+
*/
|
|
161
|
+
export interface TargetingUIHint {
|
|
162
|
+
/** Type of selection UI to show */
|
|
163
|
+
selectionType: "single" | "multiple" | "automatic" | "none";
|
|
164
|
+
|
|
165
|
+
/** Minimum number of selections required */
|
|
166
|
+
minSelections: number;
|
|
167
|
+
|
|
168
|
+
/** Maximum number of selections allowed */
|
|
169
|
+
maxSelections: number | "unlimited";
|
|
170
|
+
|
|
171
|
+
/** Human-readable prompt for the player */
|
|
172
|
+
prompt: string;
|
|
173
|
+
|
|
174
|
+
/** Whether selection is optional (can select 0) */
|
|
175
|
+
optional: boolean;
|
|
176
|
+
|
|
177
|
+
/** Zone(s) to highlight in the UI */
|
|
178
|
+
highlightZones: string[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Player Targeting
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Player target specification (separate from card targeting)
|
|
187
|
+
*
|
|
188
|
+
* Used when effects target players rather than cards.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* - "controller": The player who controls the source card
|
|
192
|
+
* - "opponent": A single opponent
|
|
193
|
+
* - "each-player": All players
|
|
194
|
+
* - "each-opponent": All opponents
|
|
195
|
+
* - "chosen-player": Player selects which player to target
|
|
196
|
+
*/
|
|
197
|
+
export type PlayerTargetDSL =
|
|
198
|
+
| "controller" // Player who controls the source
|
|
199
|
+
| "opponent" // Single opponent (or active opponent in 1v1)
|
|
200
|
+
| "each-player" // All players including controller
|
|
201
|
+
| "each-opponent" // All opponents
|
|
202
|
+
| "chosen-player"; // Controller chooses which player
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// Type Utilities
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Extract the minimum count from a TargetCount specification
|
|
210
|
+
*/
|
|
211
|
+
export function getMinCount(count: TargetCount | undefined): number {
|
|
212
|
+
if (count === undefined) return 1;
|
|
213
|
+
if (count === "all") return 0;
|
|
214
|
+
if (typeof count === "number") return count;
|
|
215
|
+
if ("exactly" in count) return count.exactly;
|
|
216
|
+
if ("upTo" in count) return 0;
|
|
217
|
+
if ("atLeast" in count) return count.atLeast;
|
|
218
|
+
if ("between" in count) return count.between[0];
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Extract the maximum count from a TargetCount specification
|
|
224
|
+
*/
|
|
225
|
+
export function getMaxCount(
|
|
226
|
+
count: TargetCount | undefined,
|
|
227
|
+
): number | "unlimited" {
|
|
228
|
+
if (count === undefined) return 1;
|
|
229
|
+
if (count === "all") return "unlimited";
|
|
230
|
+
if (typeof count === "number") return count;
|
|
231
|
+
if ("exactly" in count) return count.exactly;
|
|
232
|
+
if ("upTo" in count) return count.upTo;
|
|
233
|
+
if ("atLeast" in count) return "unlimited";
|
|
234
|
+
if ("between" in count) return count.between[1];
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Check if a target count is optional (allows selecting 0)
|
|
240
|
+
*/
|
|
241
|
+
export function isOptionalCount(count: TargetCount | undefined): boolean {
|
|
242
|
+
return getMinCount(count) === 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if a selector requires player interaction
|
|
247
|
+
*/
|
|
248
|
+
export function requiresPlayerChoice(selector: SelectorScope): boolean {
|
|
249
|
+
return selector === "chosen";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if a selector targets multiple cards
|
|
254
|
+
*/
|
|
255
|
+
export function isMultipleTargetSelector(selector: SelectorScope): boolean {
|
|
256
|
+
return selector === "all" || selector === "each";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ============================================================================
|
|
260
|
+
// Default Values
|
|
261
|
+
// ============================================================================
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Default target DSL for a single chosen card
|
|
265
|
+
*/
|
|
266
|
+
export const DEFAULT_SINGLE_TARGET: Partial<TargetDSL> = {
|
|
267
|
+
selector: "chosen",
|
|
268
|
+
count: 1,
|
|
269
|
+
owner: "any",
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Default target DSL for self-targeting
|
|
274
|
+
*/
|
|
275
|
+
export const DEFAULT_SELF_TARGET: Partial<TargetDSL> = {
|
|
276
|
+
selector: "self",
|
|
277
|
+
count: 1,
|
|
278
|
+
context: { self: true },
|
|
279
|
+
};
|