@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,306 @@
|
|
|
1
|
+
import { produce } from "immer";
|
|
2
|
+
import seedrandom from "seedrandom";
|
|
3
|
+
import type { CardId } from "../types";
|
|
4
|
+
import type { Zone } from "./zone";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adds a card to a zone
|
|
8
|
+
* @param zone - Target zone
|
|
9
|
+
* @param cardId - Card to add
|
|
10
|
+
* @param position - Optional position for ordered zones
|
|
11
|
+
* @returns Updated zone
|
|
12
|
+
* @throws Error if zone is at maximum size
|
|
13
|
+
*/
|
|
14
|
+
export function addCard(zone: Zone, cardId: CardId, position?: number): Zone {
|
|
15
|
+
if (
|
|
16
|
+
zone.config.maxSize !== undefined &&
|
|
17
|
+
zone.cards.length >= zone.config.maxSize
|
|
18
|
+
) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Cannot add card: zone is at maximum size (${zone.config.maxSize})`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return produce(zone, (draft) => {
|
|
25
|
+
if (position !== undefined) {
|
|
26
|
+
draft.cards.splice(position, 0, cardId);
|
|
27
|
+
} else {
|
|
28
|
+
draft.cards.push(cardId);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Removes a card from a zone
|
|
35
|
+
* @param zone - Source zone
|
|
36
|
+
* @param cardId - Card to remove
|
|
37
|
+
* @returns Updated zone
|
|
38
|
+
* @throws Error if card not found in zone
|
|
39
|
+
*/
|
|
40
|
+
export function removeCard(zone: Zone, cardId: CardId): Zone {
|
|
41
|
+
const index = zone.cards.indexOf(cardId);
|
|
42
|
+
if (index === -1) {
|
|
43
|
+
throw new Error(`Card ${cardId} not found in zone ${zone.config.id}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return produce(zone, (draft) => {
|
|
47
|
+
draft.cards.splice(index, 1);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Moves a card from one zone to another
|
|
53
|
+
* @param fromZone - Source zone
|
|
54
|
+
* @param toZone - Destination zone
|
|
55
|
+
* @param cardId - Card to move
|
|
56
|
+
* @param position - Optional position in destination zone
|
|
57
|
+
* @returns Updated zones
|
|
58
|
+
*/
|
|
59
|
+
export function moveCard(
|
|
60
|
+
fromZone: Zone,
|
|
61
|
+
toZone: Zone,
|
|
62
|
+
cardId: CardId,
|
|
63
|
+
position?: number,
|
|
64
|
+
): { fromZone: Zone; toZone: Zone } {
|
|
65
|
+
const updatedFrom = removeCard(fromZone, cardId);
|
|
66
|
+
const updatedTo = addCard(toZone, cardId, position);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
fromZone: updatedFrom,
|
|
70
|
+
toZone: updatedTo,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Draws cards from deck to hand
|
|
76
|
+
* @param deck - Source deck zone
|
|
77
|
+
* @param hand - Destination hand zone
|
|
78
|
+
* @param count - Number of cards to draw
|
|
79
|
+
* @returns Updated zones and drawn cards
|
|
80
|
+
* @throws Error if not enough cards in deck
|
|
81
|
+
*/
|
|
82
|
+
export function draw(
|
|
83
|
+
deck: Zone,
|
|
84
|
+
hand: Zone,
|
|
85
|
+
count: number,
|
|
86
|
+
): { fromZone: Zone; toZone: Zone; drawnCards: CardId[] } {
|
|
87
|
+
if (deck.cards.length < count) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Cannot draw ${count} cards: only ${deck.cards.length} available in ${deck.config.id}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const drawnCards = deck.cards.slice(0, count);
|
|
94
|
+
|
|
95
|
+
const newDeck = produce(deck, (draft) => {
|
|
96
|
+
draft.cards.splice(0, count);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const newHand = produce(hand, (draft) => {
|
|
100
|
+
draft.cards.push(...drawnCards);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
fromZone: newDeck,
|
|
105
|
+
toZone: newHand,
|
|
106
|
+
drawnCards,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Shuffles a zone using seeded RNG for deterministic replay
|
|
112
|
+
* @param zone - Zone to shuffle
|
|
113
|
+
* @param seed - Random seed for determinism
|
|
114
|
+
* @returns Updated zone with shuffled cards
|
|
115
|
+
*/
|
|
116
|
+
export function shuffle(zone: Zone, seed: string): Zone {
|
|
117
|
+
const rng = seedrandom(seed);
|
|
118
|
+
|
|
119
|
+
return produce(zone, (draft) => {
|
|
120
|
+
// Fisher-Yates shuffle with seeded RNG
|
|
121
|
+
for (let i = draft.cards.length - 1; i > 0; i--) {
|
|
122
|
+
const j = Math.floor(rng() * (i + 1));
|
|
123
|
+
const cardI = draft.cards[i];
|
|
124
|
+
const cardJ = draft.cards[j];
|
|
125
|
+
if (cardI !== undefined && cardJ !== undefined) {
|
|
126
|
+
draft.cards[i] = cardJ;
|
|
127
|
+
draft.cards[j] = cardI;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Searches a zone for cards matching a filter
|
|
135
|
+
* @param zone - Zone to search
|
|
136
|
+
* @param filter - Filter function
|
|
137
|
+
* @returns Array of matching card IDs
|
|
138
|
+
*/
|
|
139
|
+
export function search(
|
|
140
|
+
zone: Zone,
|
|
141
|
+
filter: (cardId: CardId) => boolean,
|
|
142
|
+
): CardId[] {
|
|
143
|
+
return zone.cards.filter(filter);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Peeks at the top N cards of a zone without removing them
|
|
148
|
+
* @param zone - Zone to peek at
|
|
149
|
+
* @param count - Number of cards to peek
|
|
150
|
+
* @returns Array of card IDs
|
|
151
|
+
*/
|
|
152
|
+
export function peek(zone: Zone, count: number): CardId[] {
|
|
153
|
+
return zone.cards.slice(0, count);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Mills cards from deck to graveyard
|
|
158
|
+
* @param deck - Source deck zone
|
|
159
|
+
* @param graveyard - Destination graveyard zone
|
|
160
|
+
* @param count - Number of cards to mill
|
|
161
|
+
* @returns Updated zones and milled cards
|
|
162
|
+
*/
|
|
163
|
+
export function mill(
|
|
164
|
+
deck: Zone,
|
|
165
|
+
graveyard: Zone,
|
|
166
|
+
count: number,
|
|
167
|
+
): { fromZone: Zone; toZone: Zone; milledCards: CardId[] } {
|
|
168
|
+
const milledCards = deck.cards.slice(0, count);
|
|
169
|
+
|
|
170
|
+
const newDeck = produce(deck, (draft) => {
|
|
171
|
+
draft.cards.splice(0, count);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const newGraveyard = produce(graveyard, (draft) => {
|
|
175
|
+
draft.cards.push(...milledCards);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
fromZone: newDeck,
|
|
180
|
+
toZone: newGraveyard,
|
|
181
|
+
milledCards,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Reveals cards (makes them temporarily visible)
|
|
187
|
+
* @param cardIds - Cards to reveal
|
|
188
|
+
* @returns Array of revealed card IDs
|
|
189
|
+
*/
|
|
190
|
+
export function reveal(cardIds: CardId[]): CardId[] {
|
|
191
|
+
return [...cardIds];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Gets the number of cards in a zone
|
|
196
|
+
* @param zone - Zone to query
|
|
197
|
+
* @returns Number of cards
|
|
198
|
+
*/
|
|
199
|
+
export function getZoneSize(zone: Zone): number {
|
|
200
|
+
return zone.cards.length;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Gets all cards in a zone
|
|
205
|
+
* @param zone - Zone to query
|
|
206
|
+
* @returns Array of card IDs
|
|
207
|
+
*/
|
|
208
|
+
export function getCardsInZone(zone: Zone): CardId[] {
|
|
209
|
+
return [...zone.cards];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Gets the top card of a zone
|
|
214
|
+
* @param zone - Zone to query
|
|
215
|
+
* @returns Top card ID or undefined if empty
|
|
216
|
+
*/
|
|
217
|
+
export function getTopCard(zone: Zone): CardId | undefined {
|
|
218
|
+
return zone.cards[0];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Gets the bottom card of a zone
|
|
223
|
+
* @param zone - Zone to query
|
|
224
|
+
* @returns Bottom card ID or undefined if empty
|
|
225
|
+
*/
|
|
226
|
+
export function getBottomCard(zone: Zone): CardId | undefined {
|
|
227
|
+
return zone.cards[zone.cards.length - 1];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Checks if a card is in a zone
|
|
232
|
+
* @param zone - Zone to check
|
|
233
|
+
* @param cardId - Card to look for
|
|
234
|
+
* @returns True if card is in zone
|
|
235
|
+
*/
|
|
236
|
+
export function isCardInZone(zone: Zone, cardId: CardId): boolean {
|
|
237
|
+
return zone.cards.includes(cardId);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Adds a card to the top of a zone
|
|
242
|
+
* @param zone - Target zone
|
|
243
|
+
* @param cardId - Card to add
|
|
244
|
+
* @returns Updated zone
|
|
245
|
+
* @throws Error if zone is at maximum size
|
|
246
|
+
*/
|
|
247
|
+
export function addCardToTop(zone: Zone, cardId: CardId): Zone {
|
|
248
|
+
if (
|
|
249
|
+
zone.config.maxSize !== undefined &&
|
|
250
|
+
zone.cards.length >= zone.config.maxSize
|
|
251
|
+
) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Cannot add card: zone is at maximum size (${zone.config.maxSize})`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return produce(zone, (draft) => {
|
|
258
|
+
draft.cards.unshift(cardId);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Adds a card to the bottom of a zone
|
|
264
|
+
* @param zone - Target zone
|
|
265
|
+
* @param cardId - Card to add
|
|
266
|
+
* @returns Updated zone
|
|
267
|
+
* @throws Error if zone is at maximum size
|
|
268
|
+
*/
|
|
269
|
+
export function addCardToBottom(zone: Zone, cardId: CardId): Zone {
|
|
270
|
+
if (
|
|
271
|
+
zone.config.maxSize !== undefined &&
|
|
272
|
+
zone.cards.length >= zone.config.maxSize
|
|
273
|
+
) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Cannot add card: zone is at maximum size (${zone.config.maxSize})`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return produce(zone, (draft) => {
|
|
280
|
+
draft.cards.push(cardId);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Clears all cards from a zone
|
|
286
|
+
* @param zone - Zone to clear
|
|
287
|
+
* @returns Updated zone with no cards
|
|
288
|
+
*/
|
|
289
|
+
export function clearZone(zone: Zone): Zone {
|
|
290
|
+
return produce(zone, (draft) => {
|
|
291
|
+
draft.cards = [];
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Finds the first zone containing a card
|
|
297
|
+
* @param cardId - Card to find
|
|
298
|
+
* @param zones - Zones to search
|
|
299
|
+
* @returns First zone containing the card, or undefined if not found
|
|
300
|
+
*/
|
|
301
|
+
export function findCardInZones(
|
|
302
|
+
cardId: CardId,
|
|
303
|
+
zones: Zone[],
|
|
304
|
+
): Zone | undefined {
|
|
305
|
+
return zones.find((zone) => isCardInZone(zone, cardId));
|
|
306
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createCardId, createPlayerId, createZoneId } from "../types";
|
|
3
|
+
import { createZone } from "./zone-factory";
|
|
4
|
+
import {
|
|
5
|
+
createPlayerZones,
|
|
6
|
+
getCardZone,
|
|
7
|
+
moveCardInState,
|
|
8
|
+
} from "./zone-state-helpers";
|
|
9
|
+
|
|
10
|
+
describe("Zone State Helpers", () => {
|
|
11
|
+
describe("createPlayerZones", () => {
|
|
12
|
+
it("should create zones for each player with factory", () => {
|
|
13
|
+
const p1 = createPlayerId("p1");
|
|
14
|
+
const p2 = createPlayerId("p2");
|
|
15
|
+
const players = [p1, p2];
|
|
16
|
+
|
|
17
|
+
const zones = createPlayerZones(players, () => [] as string[]);
|
|
18
|
+
|
|
19
|
+
expect(zones[p1]).toBeDefined();
|
|
20
|
+
expect(zones[p2]).toBeDefined();
|
|
21
|
+
expect(Object.keys(zones)).toHaveLength(2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should initialize with default values", () => {
|
|
25
|
+
const p1 = createPlayerId("p1");
|
|
26
|
+
const p2 = createPlayerId("p2");
|
|
27
|
+
const players = [p1, p2];
|
|
28
|
+
|
|
29
|
+
const zones = createPlayerZones(players, () => []);
|
|
30
|
+
|
|
31
|
+
expect(zones[p1]).toEqual([]);
|
|
32
|
+
expect(zones[p2]).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should initialize with custom factory function", () => {
|
|
36
|
+
const p1 = createPlayerId("p1");
|
|
37
|
+
const p2 = createPlayerId("p2");
|
|
38
|
+
const players = [p1, p2];
|
|
39
|
+
|
|
40
|
+
const zones = createPlayerZones(players, () => ({ count: 0 }));
|
|
41
|
+
|
|
42
|
+
expect(zones[p1]).toEqual({ count: 0 });
|
|
43
|
+
expect(zones[p2]).toEqual({ count: 0 });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should call factory function for each player", () => {
|
|
47
|
+
const players = [createPlayerId("p1"), createPlayerId("p2")];
|
|
48
|
+
let callCount = 0;
|
|
49
|
+
|
|
50
|
+
createPlayerZones(players, () => {
|
|
51
|
+
callCount++;
|
|
52
|
+
return [];
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(callCount).toBe(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should handle single player", () => {
|
|
59
|
+
const p1 = createPlayerId("p1");
|
|
60
|
+
const players = [p1];
|
|
61
|
+
|
|
62
|
+
const zones = createPlayerZones(players, () => [] as string[]);
|
|
63
|
+
|
|
64
|
+
expect(Object.keys(zones)).toHaveLength(1);
|
|
65
|
+
expect(zones[p1]).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should handle empty players array", () => {
|
|
69
|
+
const players: ReturnType<typeof createPlayerId>[] = [];
|
|
70
|
+
|
|
71
|
+
const zones = createPlayerZones(players, () => [] as string[]);
|
|
72
|
+
|
|
73
|
+
expect(Object.keys(zones)).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("moveCardInState", () => {
|
|
78
|
+
it("should move card between zones in flat state", () => {
|
|
79
|
+
const _playerId = createPlayerId("p1");
|
|
80
|
+
const cardId = createCardId("card-1");
|
|
81
|
+
|
|
82
|
+
const state = {
|
|
83
|
+
hand: createZone(
|
|
84
|
+
{
|
|
85
|
+
id: createZoneId("hand"),
|
|
86
|
+
name: "Hand",
|
|
87
|
+
visibility: "private",
|
|
88
|
+
ordered: false,
|
|
89
|
+
},
|
|
90
|
+
[cardId, createCardId("card-2")],
|
|
91
|
+
),
|
|
92
|
+
deck: createZone({
|
|
93
|
+
id: createZoneId("deck"),
|
|
94
|
+
name: "Deck",
|
|
95
|
+
visibility: "secret",
|
|
96
|
+
ordered: true,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const newState = moveCardInState(state, "hand", "deck", cardId);
|
|
101
|
+
|
|
102
|
+
expect(newState.hand.cards).not.toContain(cardId);
|
|
103
|
+
expect(newState.hand.cards).toHaveLength(1);
|
|
104
|
+
expect(newState.deck.cards).toContain(cardId);
|
|
105
|
+
expect(newState.deck.cards).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should preserve other zones in state", () => {
|
|
109
|
+
const cardId = createCardId("card-1");
|
|
110
|
+
|
|
111
|
+
const state = {
|
|
112
|
+
hand: createZone(
|
|
113
|
+
{
|
|
114
|
+
id: createZoneId("hand"),
|
|
115
|
+
name: "Hand",
|
|
116
|
+
visibility: "private",
|
|
117
|
+
ordered: false,
|
|
118
|
+
},
|
|
119
|
+
[cardId],
|
|
120
|
+
),
|
|
121
|
+
deck: createZone({
|
|
122
|
+
id: createZoneId("deck"),
|
|
123
|
+
name: "Deck",
|
|
124
|
+
visibility: "secret",
|
|
125
|
+
ordered: true,
|
|
126
|
+
}),
|
|
127
|
+
graveyard: createZone(
|
|
128
|
+
{
|
|
129
|
+
id: createZoneId("graveyard"),
|
|
130
|
+
name: "Graveyard",
|
|
131
|
+
visibility: "public",
|
|
132
|
+
ordered: true,
|
|
133
|
+
},
|
|
134
|
+
[createCardId("card-3")],
|
|
135
|
+
),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const newState = moveCardInState(state, "hand", "deck", cardId);
|
|
139
|
+
|
|
140
|
+
expect(newState.graveyard).toBe(state.graveyard);
|
|
141
|
+
expect(newState.graveyard.cards).toHaveLength(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should maintain immutability", () => {
|
|
145
|
+
const cardId = createCardId("card-1");
|
|
146
|
+
|
|
147
|
+
const state = {
|
|
148
|
+
hand: createZone(
|
|
149
|
+
{
|
|
150
|
+
id: createZoneId("hand"),
|
|
151
|
+
name: "Hand",
|
|
152
|
+
visibility: "private",
|
|
153
|
+
ordered: false,
|
|
154
|
+
},
|
|
155
|
+
[cardId],
|
|
156
|
+
),
|
|
157
|
+
deck: createZone({
|
|
158
|
+
id: createZoneId("deck"),
|
|
159
|
+
name: "Deck",
|
|
160
|
+
visibility: "secret",
|
|
161
|
+
ordered: true,
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const originalHandCards = state.hand.cards;
|
|
166
|
+
const originalDeckCards = state.deck.cards;
|
|
167
|
+
|
|
168
|
+
const newState = moveCardInState(state, "hand", "deck", cardId);
|
|
169
|
+
|
|
170
|
+
expect(state.hand.cards).toBe(originalHandCards);
|
|
171
|
+
expect(state.deck.cards).toBe(originalDeckCards);
|
|
172
|
+
expect(newState.hand).not.toBe(state.hand);
|
|
173
|
+
expect(newState.deck).not.toBe(state.deck);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should add card with optional position", () => {
|
|
177
|
+
const cardId = createCardId("card-1");
|
|
178
|
+
|
|
179
|
+
const state = {
|
|
180
|
+
hand: createZone(
|
|
181
|
+
{
|
|
182
|
+
id: createZoneId("hand"),
|
|
183
|
+
name: "Hand",
|
|
184
|
+
visibility: "private",
|
|
185
|
+
ordered: false,
|
|
186
|
+
},
|
|
187
|
+
[cardId],
|
|
188
|
+
),
|
|
189
|
+
deck: createZone(
|
|
190
|
+
{
|
|
191
|
+
id: createZoneId("deck"),
|
|
192
|
+
name: "Deck",
|
|
193
|
+
visibility: "secret",
|
|
194
|
+
ordered: true,
|
|
195
|
+
},
|
|
196
|
+
[createCardId("card-2")],
|
|
197
|
+
),
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const newState = moveCardInState(state, "hand", "deck", cardId, 0);
|
|
201
|
+
|
|
202
|
+
expect(newState.deck.cards[0]).toBe(cardId);
|
|
203
|
+
expect(newState.deck.cards[1]).toBe(createCardId("card-2"));
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("getCardZone", () => {
|
|
208
|
+
it("should find card in first zone", () => {
|
|
209
|
+
const cardId = createCardId("card-1");
|
|
210
|
+
|
|
211
|
+
const state = {
|
|
212
|
+
hand: createZone(
|
|
213
|
+
{
|
|
214
|
+
id: createZoneId("hand"),
|
|
215
|
+
name: "Hand",
|
|
216
|
+
visibility: "private",
|
|
217
|
+
ordered: false,
|
|
218
|
+
},
|
|
219
|
+
[cardId, createCardId("card-2")],
|
|
220
|
+
),
|
|
221
|
+
deck: createZone({
|
|
222
|
+
id: createZoneId("deck"),
|
|
223
|
+
name: "Deck",
|
|
224
|
+
visibility: "secret",
|
|
225
|
+
ordered: true,
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const zoneName = getCardZone(state, cardId);
|
|
230
|
+
|
|
231
|
+
expect(zoneName).toBe("hand");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should find card in later zone", () => {
|
|
235
|
+
const cardId = createCardId("card-3");
|
|
236
|
+
|
|
237
|
+
const state = {
|
|
238
|
+
hand: createZone(
|
|
239
|
+
{
|
|
240
|
+
id: createZoneId("hand"),
|
|
241
|
+
name: "Hand",
|
|
242
|
+
visibility: "private",
|
|
243
|
+
ordered: false,
|
|
244
|
+
},
|
|
245
|
+
[createCardId("card-1")],
|
|
246
|
+
),
|
|
247
|
+
deck: createZone(
|
|
248
|
+
{
|
|
249
|
+
id: createZoneId("deck"),
|
|
250
|
+
name: "Deck",
|
|
251
|
+
visibility: "secret",
|
|
252
|
+
ordered: true,
|
|
253
|
+
},
|
|
254
|
+
[createCardId("card-2"), cardId],
|
|
255
|
+
),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const zoneName = getCardZone(state, cardId);
|
|
259
|
+
|
|
260
|
+
expect(zoneName).toBe("deck");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should return undefined when card not found", () => {
|
|
264
|
+
const state = {
|
|
265
|
+
hand: createZone(
|
|
266
|
+
{
|
|
267
|
+
id: createZoneId("hand"),
|
|
268
|
+
name: "Hand",
|
|
269
|
+
visibility: "private",
|
|
270
|
+
ordered: false,
|
|
271
|
+
},
|
|
272
|
+
[createCardId("card-1")],
|
|
273
|
+
),
|
|
274
|
+
deck: createZone({
|
|
275
|
+
id: createZoneId("deck"),
|
|
276
|
+
name: "Deck",
|
|
277
|
+
visibility: "secret",
|
|
278
|
+
ordered: true,
|
|
279
|
+
}),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const zoneName = getCardZone(state, createCardId("missing"));
|
|
283
|
+
|
|
284
|
+
expect(zoneName).toBeUndefined();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should return first zone if card in multiple zones", () => {
|
|
288
|
+
const cardId = createCardId("card-1");
|
|
289
|
+
|
|
290
|
+
const state = {
|
|
291
|
+
hand: createZone(
|
|
292
|
+
{
|
|
293
|
+
id: createZoneId("hand"),
|
|
294
|
+
name: "Hand",
|
|
295
|
+
visibility: "private",
|
|
296
|
+
ordered: false,
|
|
297
|
+
},
|
|
298
|
+
[cardId],
|
|
299
|
+
),
|
|
300
|
+
deck: createZone(
|
|
301
|
+
{
|
|
302
|
+
id: createZoneId("deck"),
|
|
303
|
+
name: "Deck",
|
|
304
|
+
visibility: "secret",
|
|
305
|
+
ordered: true,
|
|
306
|
+
},
|
|
307
|
+
[cardId],
|
|
308
|
+
),
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const zoneName = getCardZone(state, cardId);
|
|
312
|
+
|
|
313
|
+
expect(zoneName).toBe("hand");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("should handle empty zones", () => {
|
|
317
|
+
const state = {
|
|
318
|
+
hand: createZone({
|
|
319
|
+
id: createZoneId("hand"),
|
|
320
|
+
name: "Hand",
|
|
321
|
+
visibility: "private",
|
|
322
|
+
ordered: false,
|
|
323
|
+
}),
|
|
324
|
+
deck: createZone({
|
|
325
|
+
id: createZoneId("deck"),
|
|
326
|
+
name: "Deck",
|
|
327
|
+
visibility: "secret",
|
|
328
|
+
ordered: true,
|
|
329
|
+
}),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const zoneName = getCardZone(state, createCardId("card-1"));
|
|
333
|
+
|
|
334
|
+
expect(zoneName).toBeUndefined();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|