@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,468 @@
|
|
|
1
|
+
import type { Logger } from "../logging";
|
|
2
|
+
import type { CardId, PlayerId } from "../types";
|
|
3
|
+
import type { InternalState } from "../types/state";
|
|
4
|
+
import type { CardOperations } from "./card-operations";
|
|
5
|
+
import type { CounterOperations } from "./counter-operations";
|
|
6
|
+
import type { GameOperations } from "./game-operations";
|
|
7
|
+
import type { ZoneOperations } from "./zone-operations";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a ZoneOperations implementation backed by InternalState
|
|
11
|
+
*
|
|
12
|
+
* @param state - Internal state to operate on (will be mutated)
|
|
13
|
+
* @param logger - Optional logger for TRACE-level logging
|
|
14
|
+
* @returns ZoneOperations implementation
|
|
15
|
+
*/
|
|
16
|
+
export const createZoneOperations = <TCardDef, TCardMeta>(
|
|
17
|
+
state: InternalState<TCardDef, TCardMeta>,
|
|
18
|
+
logger?: Logger,
|
|
19
|
+
): ZoneOperations => {
|
|
20
|
+
const zoneOps: ZoneOperations = {
|
|
21
|
+
moveCard: ({ cardId, targetZoneId, position = "bottom" }) => {
|
|
22
|
+
logger?.trace("Moving card", { cardId, targetZoneId, position });
|
|
23
|
+
// Find current zone and remove card
|
|
24
|
+
let sourceZoneId: string | undefined;
|
|
25
|
+
for (const zoneId in state.zones) {
|
|
26
|
+
const zone = state.zones[zoneId];
|
|
27
|
+
if (!zone) continue;
|
|
28
|
+
const index = zone.cardIds.indexOf(cardId);
|
|
29
|
+
if (index !== -1) {
|
|
30
|
+
zone.cardIds.splice(index, 1);
|
|
31
|
+
sourceZoneId = zoneId;
|
|
32
|
+
|
|
33
|
+
// Update positions in source zone if ordered
|
|
34
|
+
if (zone.config.ordered) {
|
|
35
|
+
for (let i = index; i < zone.cardIds.length; i++) {
|
|
36
|
+
const cid = zone.cardIds[i];
|
|
37
|
+
if (!cid) continue;
|
|
38
|
+
if (state.cards[cid]) {
|
|
39
|
+
state.cards[cid].position = i;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Add to target zone
|
|
48
|
+
const targetZone = state.zones[targetZoneId as string];
|
|
49
|
+
if (!targetZone) {
|
|
50
|
+
throw new Error(`Target zone ${targetZoneId} does not exist`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let targetPosition: number | undefined;
|
|
54
|
+
|
|
55
|
+
if (position === "top") {
|
|
56
|
+
targetZone.cardIds.unshift(cardId);
|
|
57
|
+
targetPosition = 0;
|
|
58
|
+
|
|
59
|
+
// Update positions of other cards in ordered zones
|
|
60
|
+
if (targetZone.config.ordered) {
|
|
61
|
+
for (let i = 1; i < targetZone.cardIds.length; i++) {
|
|
62
|
+
const cid = targetZone.cardIds[i] as string;
|
|
63
|
+
if (state.cards[cid]) {
|
|
64
|
+
state.cards[cid].position = i;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else if (position === "bottom") {
|
|
69
|
+
targetZone.cardIds.push(cardId);
|
|
70
|
+
targetPosition = targetZone.config.ordered
|
|
71
|
+
? targetZone.cardIds.length - 1
|
|
72
|
+
: undefined;
|
|
73
|
+
} else {
|
|
74
|
+
// Numeric position
|
|
75
|
+
const idx = position as number;
|
|
76
|
+
targetZone.cardIds.splice(idx, 0, cardId);
|
|
77
|
+
targetPosition = targetZone.config.ordered ? idx : undefined;
|
|
78
|
+
|
|
79
|
+
// Update positions of cards after insertion point
|
|
80
|
+
if (targetZone.config.ordered) {
|
|
81
|
+
for (let i = idx + 1; i < targetZone.cardIds.length; i++) {
|
|
82
|
+
const cid = targetZone.cardIds[i] as string;
|
|
83
|
+
if (state.cards[cid]) {
|
|
84
|
+
state.cards[cid].position = i;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Update card's zone and position
|
|
91
|
+
const card = state.cards[cardId as string];
|
|
92
|
+
if (card) {
|
|
93
|
+
card.zone = targetZoneId;
|
|
94
|
+
card.position = targetPosition;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
getCardsInZone: (zoneId, ownerId?) => {
|
|
99
|
+
const zone = state.zones[zoneId as string];
|
|
100
|
+
if (!zone) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let cards = zone.cardIds;
|
|
105
|
+
|
|
106
|
+
// Filter by owner if specified
|
|
107
|
+
if (ownerId !== undefined) {
|
|
108
|
+
cards = cards.filter((cardId) => {
|
|
109
|
+
const card = state.cards[cardId as string];
|
|
110
|
+
return card && card.owner === ownerId;
|
|
111
|
+
}) as CardId[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Return a copy to prevent external mutation
|
|
115
|
+
return [...cards];
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
shuffleZone: (zoneId, ownerId?) => {
|
|
119
|
+
const zone = state.zones[zoneId as string];
|
|
120
|
+
if (!zone) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Simple Fisher-Yates shuffle
|
|
125
|
+
// Note: In production, this should use a seeded RNG for determinism
|
|
126
|
+
const cards = [...zone.cardIds];
|
|
127
|
+
for (let i = cards.length - 1; i > 0; i--) {
|
|
128
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
129
|
+
const temp = cards[i];
|
|
130
|
+
const swapCard = cards[j];
|
|
131
|
+
if (temp && swapCard) {
|
|
132
|
+
cards[i] = swapCard;
|
|
133
|
+
cards[j] = temp;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
zone.cardIds = cards;
|
|
138
|
+
|
|
139
|
+
// Update positions if ordered
|
|
140
|
+
if (zone.config.ordered) {
|
|
141
|
+
for (let i = 0; i < cards.length; i++) {
|
|
142
|
+
const cardId = cards[i] as string;
|
|
143
|
+
if (state.cards[cardId]) {
|
|
144
|
+
state.cards[cardId].position = i;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
getCardZone: (cardId) => {
|
|
151
|
+
const card = state.cards[cardId as string];
|
|
152
|
+
return card?.zone;
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
drawCards: ({ from, to, count, playerId }) => {
|
|
156
|
+
const sourceCards = zoneOps.getCardsInZone(from, playerId);
|
|
157
|
+
const drawnCards: CardId[] = [];
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < count && i < sourceCards.length; i++) {
|
|
160
|
+
const cardId = sourceCards[i];
|
|
161
|
+
if (cardId) {
|
|
162
|
+
zoneOps.moveCard({
|
|
163
|
+
cardId,
|
|
164
|
+
targetZoneId: to,
|
|
165
|
+
position: "bottom",
|
|
166
|
+
});
|
|
167
|
+
drawnCards.push(cardId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return drawnCards;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
mulligan: ({ hand, deck, drawCount, playerId }) => {
|
|
175
|
+
const handCards = zoneOps.getCardsInZone(hand, playerId);
|
|
176
|
+
|
|
177
|
+
// Move all hand cards back to deck
|
|
178
|
+
for (const cardId of handCards) {
|
|
179
|
+
zoneOps.moveCard({
|
|
180
|
+
cardId,
|
|
181
|
+
targetZoneId: deck,
|
|
182
|
+
position: "bottom",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Shuffle deck
|
|
187
|
+
zoneOps.shuffleZone(deck, playerId);
|
|
188
|
+
|
|
189
|
+
// Draw new hand
|
|
190
|
+
zoneOps.drawCards({ from: deck, to: hand, count: drawCount, playerId });
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
bulkMove: ({ from, to, count, playerId, position = "bottom" }) => {
|
|
194
|
+
const sourceCards = zoneOps.getCardsInZone(from, playerId);
|
|
195
|
+
const movedCards: CardId[] = [];
|
|
196
|
+
|
|
197
|
+
for (let i = 0; i < count && i < sourceCards.length; i++) {
|
|
198
|
+
const cardId = sourceCards[i];
|
|
199
|
+
if (cardId) {
|
|
200
|
+
zoneOps.moveCard({
|
|
201
|
+
cardId,
|
|
202
|
+
targetZoneId: to,
|
|
203
|
+
position,
|
|
204
|
+
});
|
|
205
|
+
movedCards.push(cardId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return movedCards;
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
createDeck: ({ zoneId, playerId, cardCount, shuffle = false }) => {
|
|
213
|
+
const createdCards: CardId[] = [];
|
|
214
|
+
|
|
215
|
+
// Create card instances
|
|
216
|
+
for (let i = 0; i < cardCount; i++) {
|
|
217
|
+
const cardId = `${playerId}-${zoneId}-${i}` as CardId;
|
|
218
|
+
createdCards.push(cardId);
|
|
219
|
+
|
|
220
|
+
// Add card to internal state
|
|
221
|
+
state.cards[cardId as string] = {
|
|
222
|
+
definitionId: "placeholder", // Games can customize this
|
|
223
|
+
owner: playerId,
|
|
224
|
+
controller: playerId, // Initially controller equals owner
|
|
225
|
+
zone: zoneId,
|
|
226
|
+
position: i,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Add to zone
|
|
230
|
+
const zone = state.zones[zoneId as string];
|
|
231
|
+
if (zone) {
|
|
232
|
+
zone.cardIds.push(cardId);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Shuffle if requested
|
|
237
|
+
if (shuffle) {
|
|
238
|
+
zoneOps.shuffleZone(zoneId, playerId);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return createdCards;
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return zoneOps;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Create a CardOperations implementation backed by InternalState
|
|
250
|
+
*
|
|
251
|
+
* @param state - Internal state to operate on (will be mutated)
|
|
252
|
+
* @param logger - Optional logger for TRACE-level logging
|
|
253
|
+
* @returns CardOperations implementation
|
|
254
|
+
*/
|
|
255
|
+
export const createCardOperations = <TCardDef, TCardMeta>(
|
|
256
|
+
state: InternalState<TCardDef, TCardMeta>,
|
|
257
|
+
logger?: Logger,
|
|
258
|
+
): CardOperations<TCardMeta> => {
|
|
259
|
+
return {
|
|
260
|
+
getCardMeta: (cardId) => {
|
|
261
|
+
logger?.trace("Getting card meta", { cardId });
|
|
262
|
+
return (state.cardMetas[cardId as string] || {}) as Partial<TCardMeta>;
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
updateCardMeta: (cardId, meta) => {
|
|
266
|
+
logger?.trace("Updating card meta", { cardId, updates: meta });
|
|
267
|
+
const existing = state.cardMetas[cardId as string];
|
|
268
|
+
if (existing) {
|
|
269
|
+
Object.assign(existing, meta);
|
|
270
|
+
} else {
|
|
271
|
+
state.cardMetas[cardId as string] = meta as TCardMeta;
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
setCardMeta: (cardId, meta) => {
|
|
276
|
+
logger?.trace("Setting card meta", { cardId });
|
|
277
|
+
state.cardMetas[cardId as string] = meta;
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
getCardOwner: (cardId) => {
|
|
281
|
+
const card = state.cards[cardId as string];
|
|
282
|
+
return card?.owner;
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
queryCards: (predicate) => {
|
|
286
|
+
const results: CardId[] = [];
|
|
287
|
+
for (const cardId in state.cardMetas) {
|
|
288
|
+
const meta = state.cardMetas[cardId];
|
|
289
|
+
if (predicate(cardId as CardId, meta as Partial<TCardMeta>)) {
|
|
290
|
+
results.push(cardId as CardId);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return results;
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Create a GameOperations implementation backed by InternalState
|
|
300
|
+
*
|
|
301
|
+
* @param state - Internal state to operate on (will be mutated)
|
|
302
|
+
* @param logger - Optional logger for TRACE-level logging
|
|
303
|
+
* @returns GameOperations implementation
|
|
304
|
+
*/
|
|
305
|
+
export const createGameOperations = <TCardDef, TCardMeta>(
|
|
306
|
+
state: InternalState<TCardDef, TCardMeta>,
|
|
307
|
+
logger?: Logger,
|
|
308
|
+
): GameOperations => {
|
|
309
|
+
return {
|
|
310
|
+
setOTP: (playerId: PlayerId) => {
|
|
311
|
+
logger?.trace("Setting OTP", { playerId });
|
|
312
|
+
state.otp = playerId;
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
getOTP: () => {
|
|
316
|
+
return state.otp;
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
setChoosingFirstPlayer: (playerId: PlayerId) => {
|
|
320
|
+
state.choosingFirstPlayer = playerId;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
getChoosingFirstPlayer: () => {
|
|
324
|
+
return state.choosingFirstPlayer;
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
setPendingMulligan: (playerIds: PlayerId[]) => {
|
|
328
|
+
state.pendingMulligan = playerIds;
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
getPendingMulligan: () => {
|
|
332
|
+
// Return copy to prevent external mutation
|
|
333
|
+
return state.pendingMulligan ? [...state.pendingMulligan] : [];
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
addPendingMulligan: (playerId: PlayerId) => {
|
|
337
|
+
if (!state.pendingMulligan) {
|
|
338
|
+
state.pendingMulligan = [playerId];
|
|
339
|
+
} else if (!state.pendingMulligan.includes(playerId)) {
|
|
340
|
+
state.pendingMulligan.push(playerId);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
removePendingMulligan: (playerId: PlayerId) => {
|
|
345
|
+
if (!state.pendingMulligan) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const index = state.pendingMulligan.indexOf(playerId);
|
|
349
|
+
if (index !== -1) {
|
|
350
|
+
state.pendingMulligan.splice(index, 1);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Internal counter state stored in cardMetas
|
|
358
|
+
* Uses a reserved key to avoid conflicts with game-specific metadata
|
|
359
|
+
*/
|
|
360
|
+
interface CounterState {
|
|
361
|
+
__counters?: Record<string, number>;
|
|
362
|
+
__flags?: Record<string, boolean>;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Create a CounterOperations implementation backed by InternalState
|
|
367
|
+
*
|
|
368
|
+
* Counters and flags are stored in cardMetas using reserved keys (__counters, __flags)
|
|
369
|
+
* to avoid conflicts with game-specific metadata.
|
|
370
|
+
*
|
|
371
|
+
* @param state - Internal state to operate on (will be mutated)
|
|
372
|
+
* @param logger - Optional logger for TRACE-level logging
|
|
373
|
+
* @returns CounterOperations implementation
|
|
374
|
+
*/
|
|
375
|
+
export const createCounterOperations = <TCardDef, TCardMeta>(
|
|
376
|
+
state: InternalState<TCardDef, TCardMeta>,
|
|
377
|
+
logger?: Logger,
|
|
378
|
+
): CounterOperations => {
|
|
379
|
+
const getCounterState = (cardId: CardId): CounterState => {
|
|
380
|
+
const meta = state.cardMetas[cardId as string];
|
|
381
|
+
if (!meta) {
|
|
382
|
+
state.cardMetas[cardId as string] = {} as TCardMeta;
|
|
383
|
+
}
|
|
384
|
+
return state.cardMetas[cardId as string] as unknown as CounterState;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
setFlag: (cardId, flag, value) => {
|
|
389
|
+
logger?.trace("Setting flag", { cardId, flag, value });
|
|
390
|
+
const counterState = getCounterState(cardId);
|
|
391
|
+
if (!counterState.__flags) {
|
|
392
|
+
counterState.__flags = {};
|
|
393
|
+
}
|
|
394
|
+
counterState.__flags[flag] = value;
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
getFlag: (cardId, flag) => {
|
|
398
|
+
const meta = state.cardMetas[cardId as string] as unknown as CounterState;
|
|
399
|
+
return meta?.__flags?.[flag] ?? false;
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
addCounter: (cardId, type, amount) => {
|
|
403
|
+
logger?.trace("Adding counter", { cardId, type, amount });
|
|
404
|
+
if (amount <= 0) return;
|
|
405
|
+
const counterState = getCounterState(cardId);
|
|
406
|
+
if (!counterState.__counters) {
|
|
407
|
+
counterState.__counters = {};
|
|
408
|
+
}
|
|
409
|
+
counterState.__counters[type] =
|
|
410
|
+
(counterState.__counters[type] ?? 0) + amount;
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
removeCounter: (cardId, type, amount) => {
|
|
414
|
+
logger?.trace("Removing counter", { cardId, type, amount });
|
|
415
|
+
if (amount <= 0) return;
|
|
416
|
+
const counterState = getCounterState(cardId);
|
|
417
|
+
if (!counterState.__counters) return;
|
|
418
|
+
const current = counterState.__counters[type] ?? 0;
|
|
419
|
+
counterState.__counters[type] = Math.max(0, current - amount);
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
getCounter: (cardId, type) => {
|
|
423
|
+
const meta = state.cardMetas[cardId as string] as unknown as CounterState;
|
|
424
|
+
return meta?.__counters?.[type] ?? 0;
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
clearCounter: (cardId, type) => {
|
|
428
|
+
logger?.trace("Clearing counter", { cardId, type });
|
|
429
|
+
const meta = state.cardMetas[cardId as string] as unknown as CounterState;
|
|
430
|
+
if (meta?.__counters) {
|
|
431
|
+
delete meta.__counters[type];
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
clearAllCounters: (cardId) => {
|
|
436
|
+
logger?.trace("Clearing all counters", { cardId });
|
|
437
|
+
const meta = state.cardMetas[cardId as string] as unknown as CounterState;
|
|
438
|
+
if (meta) {
|
|
439
|
+
delete meta.__counters;
|
|
440
|
+
delete meta.__flags;
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
getCardsWithFlag: (flag, value) => {
|
|
445
|
+
const results: CardId[] = [];
|
|
446
|
+
for (const cardId in state.cardMetas) {
|
|
447
|
+
const meta = state.cardMetas[cardId] as unknown as CounterState;
|
|
448
|
+
const flagValue = meta?.__flags?.[flag] ?? false;
|
|
449
|
+
if (flagValue === value) {
|
|
450
|
+
results.push(cardId as CardId);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return results;
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
getCardsWithCounter: (type, minValue = 1) => {
|
|
457
|
+
const results: CardId[] = [];
|
|
458
|
+
for (const cardId in state.cardMetas) {
|
|
459
|
+
const meta = state.cardMetas[cardId] as unknown as CounterState;
|
|
460
|
+
const counterValue = meta?.__counters?.[type] ?? 0;
|
|
461
|
+
if (counterValue >= minValue) {
|
|
462
|
+
results.push(cardId as CardId);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return results;
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
};
|