@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,175 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { CardId, GameId, PlayerId, ZoneId } from "./branded";
|
|
3
|
+
import {
|
|
4
|
+
createCardId,
|
|
5
|
+
createGameId,
|
|
6
|
+
createPlayerId,
|
|
7
|
+
createZoneId,
|
|
8
|
+
} from "./branded-utils";
|
|
9
|
+
|
|
10
|
+
describe("Branded Types", () => {
|
|
11
|
+
describe("createCardId", () => {
|
|
12
|
+
it("should create a CardId from a string literal", () => {
|
|
13
|
+
const cardId = createCardId("card-123");
|
|
14
|
+
expect(typeof cardId).toBe("string");
|
|
15
|
+
// Runtime value should match the input
|
|
16
|
+
expect(String(cardId)).toBe("card-123");
|
|
17
|
+
// Type assertion to verify it's properly typed
|
|
18
|
+
const _typeCheck: CardId = cardId;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should generate unique IDs when called without arguments", () => {
|
|
22
|
+
const id1 = createCardId();
|
|
23
|
+
const id2 = createCardId();
|
|
24
|
+
expect(id1).not.toBe(id2);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("should generate IDs with proper format", () => {
|
|
28
|
+
const cardId = createCardId();
|
|
29
|
+
expect(typeof cardId).toBe("string");
|
|
30
|
+
expect(cardId.length).toBeGreaterThan(0);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("createPlayerId", () => {
|
|
35
|
+
it("should create a PlayerId from a string literal", () => {
|
|
36
|
+
const playerId = createPlayerId("player-456");
|
|
37
|
+
expect(typeof playerId).toBe("string");
|
|
38
|
+
// Runtime value should match the input
|
|
39
|
+
expect(String(playerId)).toBe("player-456");
|
|
40
|
+
// Type assertion to verify it's properly typed
|
|
41
|
+
const _typeCheck: PlayerId = playerId;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should generate unique IDs when called without arguments", () => {
|
|
45
|
+
const id1 = createPlayerId();
|
|
46
|
+
const id2 = createPlayerId();
|
|
47
|
+
expect(id1).not.toBe(id2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should generate IDs with proper format", () => {
|
|
51
|
+
const playerId = createPlayerId();
|
|
52
|
+
expect(typeof playerId).toBe("string");
|
|
53
|
+
expect(playerId.length).toBeGreaterThan(0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("createGameId", () => {
|
|
58
|
+
it("should create a GameId from a string literal", () => {
|
|
59
|
+
const gameId = createGameId("game-789");
|
|
60
|
+
expect(typeof gameId).toBe("string");
|
|
61
|
+
// Runtime value should match the input
|
|
62
|
+
expect(String(gameId)).toBe("game-789");
|
|
63
|
+
// Type assertion to verify it's properly typed
|
|
64
|
+
const _typeCheck: GameId = gameId;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should generate unique IDs when called without arguments", () => {
|
|
68
|
+
const id1 = createGameId();
|
|
69
|
+
const id2 = createGameId();
|
|
70
|
+
expect(id1).not.toBe(id2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should generate IDs with proper format", () => {
|
|
74
|
+
const gameId = createGameId();
|
|
75
|
+
expect(typeof gameId).toBe("string");
|
|
76
|
+
expect(gameId.length).toBeGreaterThan(0);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("createZoneId", () => {
|
|
81
|
+
it("should create a ZoneId from a string literal", () => {
|
|
82
|
+
const zoneId = createZoneId("zone-abc");
|
|
83
|
+
expect(typeof zoneId).toBe("string");
|
|
84
|
+
// Runtime value should match the input
|
|
85
|
+
expect(String(zoneId)).toBe("zone-abc");
|
|
86
|
+
// Type assertion to verify it's properly typed
|
|
87
|
+
const _typeCheck: ZoneId = zoneId;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should generate unique IDs when called without arguments", () => {
|
|
91
|
+
const id1 = createZoneId();
|
|
92
|
+
const id2 = createZoneId();
|
|
93
|
+
expect(id1).not.toBe(id2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should generate IDs with proper format", () => {
|
|
97
|
+
const zoneId = createZoneId();
|
|
98
|
+
expect(typeof zoneId).toBe("string");
|
|
99
|
+
expect(zoneId.length).toBeGreaterThan(0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("Type Safety", () => {
|
|
104
|
+
it("should prevent mixing different ID types at compile time", () => {
|
|
105
|
+
const cardId = createCardId("id-1");
|
|
106
|
+
const playerId = createPlayerId("id-2");
|
|
107
|
+
|
|
108
|
+
// This test verifies type safety at compile time
|
|
109
|
+
// The following would fail TypeScript compilation:
|
|
110
|
+
// const wrongAssignment1: CardId = playerId;
|
|
111
|
+
// const wrongAssignment2: PlayerId = cardId;
|
|
112
|
+
|
|
113
|
+
// But we can verify runtime values are still just strings
|
|
114
|
+
expect(typeof cardId).toBe("string");
|
|
115
|
+
expect(typeof playerId).toBe("string");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should allow assignment of branded types to their brand type", () => {
|
|
119
|
+
const cardId: CardId = createCardId("card-123");
|
|
120
|
+
const acceptsCardId = (id: CardId): CardId => id;
|
|
121
|
+
|
|
122
|
+
expect(acceptsCardId(cardId)).toBe(cardId);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should work with arrays and collections", () => {
|
|
126
|
+
const cardIds: CardId[] = [
|
|
127
|
+
createCardId("1"),
|
|
128
|
+
createCardId("2"),
|
|
129
|
+
createCardId("3"),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
expect(cardIds).toHaveLength(3);
|
|
133
|
+
expect(cardIds.every((id) => typeof id === "string")).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should work with Set and Map", () => {
|
|
137
|
+
const cardIdSet = new Set<CardId>([createCardId("1"), createCardId("2")]);
|
|
138
|
+
const cardIdMap = new Map<CardId, string>([
|
|
139
|
+
[createCardId("1"), "Card One"],
|
|
140
|
+
[createCardId("2"), "Card Two"],
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
expect(cardIdSet.size).toBe(2);
|
|
144
|
+
expect(cardIdMap.size).toBe(2);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("ID Generation Consistency", () => {
|
|
149
|
+
it("should generate IDs of consistent length", () => {
|
|
150
|
+
const ids = [
|
|
151
|
+
createCardId(),
|
|
152
|
+
createPlayerId(),
|
|
153
|
+
createGameId(),
|
|
154
|
+
createZoneId(),
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const lengths = ids.map((id) => id.length);
|
|
158
|
+
expect(new Set(lengths).size).toBe(1); // All same length
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should generate URL-safe IDs", () => {
|
|
162
|
+
const ids = [
|
|
163
|
+
createCardId(),
|
|
164
|
+
createPlayerId(),
|
|
165
|
+
createGameId(),
|
|
166
|
+
createZoneId(),
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
for (const id of ids) {
|
|
170
|
+
// nanoid generates URL-safe characters: A-Za-z0-9_-
|
|
171
|
+
expect(id).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded Types
|
|
3
|
+
*
|
|
4
|
+
* Branded types provide compile-time type safety for primitive values
|
|
5
|
+
* to prevent mixing different ID types and other domain values.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare const brand: unique symbol;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base branded type that adds a compile-time brand to a value
|
|
12
|
+
*/
|
|
13
|
+
export type Brand<T, TBrand> = T & { readonly [brand]: TBrand };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Card identifier - unique ID for a card instance in the game
|
|
17
|
+
*/
|
|
18
|
+
export type CardId = Brand<string, "CardId">;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Player identifier - unique ID for a player in the game
|
|
22
|
+
*/
|
|
23
|
+
export type PlayerId = Brand<string, "PlayerId">;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Game identifier - unique ID for a game session
|
|
27
|
+
*/
|
|
28
|
+
export type GameId = Brand<string, "GameId">;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Zone identifier - unique ID for a zone in the game
|
|
32
|
+
*/
|
|
33
|
+
export type ZoneId = Brand<string, "ZoneId">;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { CardZoneConfig } from "../zones";
|
|
3
|
+
import type { CardId, PlayerId, ZoneId } from "./index";
|
|
4
|
+
import type { InternalState, IState } from "./state";
|
|
5
|
+
|
|
6
|
+
describe("InternalState", () => {
|
|
7
|
+
it("should allow defining zones with configuration and card lists", () => {
|
|
8
|
+
// Test that InternalState can hold zone data
|
|
9
|
+
type TestCardDef = { id: string; name: string };
|
|
10
|
+
type TestCardMeta = { damage?: number };
|
|
11
|
+
|
|
12
|
+
const zoneConfig: CardZoneConfig = {
|
|
13
|
+
id: "hand" as unknown as ZoneId,
|
|
14
|
+
name: "Hand",
|
|
15
|
+
visibility: "private",
|
|
16
|
+
ordered: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const internalState: InternalState<TestCardDef, TestCardMeta> = {
|
|
20
|
+
zones: {
|
|
21
|
+
hand: {
|
|
22
|
+
config: zoneConfig,
|
|
23
|
+
cardIds: ["card-1", "card-2"] as unknown as CardId[],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
cards: {},
|
|
27
|
+
cardMetas: {},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
expect(internalState.zones.hand.cardIds).toHaveLength(2);
|
|
31
|
+
expect(internalState.zones.hand.config.visibility).toBe("private");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should allow defining card instances with owner and zone", () => {
|
|
35
|
+
type TestCardDef = { id: string; name: string };
|
|
36
|
+
type TestCardMeta = { damage?: number };
|
|
37
|
+
|
|
38
|
+
const internalState: InternalState<TestCardDef, TestCardMeta> = {
|
|
39
|
+
zones: {},
|
|
40
|
+
cards: {
|
|
41
|
+
"card-1": {
|
|
42
|
+
definitionId: "pikachu",
|
|
43
|
+
owner: "player-1" as unknown as PlayerId,
|
|
44
|
+
controller: "player-1" as unknown as PlayerId,
|
|
45
|
+
zone: "hand" as unknown as ZoneId,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
cardMetas: {},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
expect(internalState.cards["card-1"].definitionId).toBe("pikachu");
|
|
52
|
+
expect(internalState.cards["card-1"].owner).toBe(
|
|
53
|
+
"player-1" as unknown as PlayerId,
|
|
54
|
+
);
|
|
55
|
+
expect(internalState.cards["card-1"].zone).toBe(
|
|
56
|
+
"hand" as unknown as ZoneId,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should allow defining card metadata for dynamic properties", () => {
|
|
61
|
+
type TestCardDef = { id: string };
|
|
62
|
+
type TestCardMeta = {
|
|
63
|
+
damage?: number;
|
|
64
|
+
exerted?: boolean;
|
|
65
|
+
effects?: string[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const internalState: InternalState<TestCardDef, TestCardMeta> = {
|
|
69
|
+
zones: {},
|
|
70
|
+
cards: {},
|
|
71
|
+
cardMetas: {
|
|
72
|
+
"card-1": {
|
|
73
|
+
damage: 5,
|
|
74
|
+
exerted: true,
|
|
75
|
+
effects: ["poisoned"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
expect(internalState.cardMetas["card-1"].damage).toBe(5);
|
|
81
|
+
expect(internalState.cardMetas["card-1"].exerted).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should allow position tracking for ordered zones", () => {
|
|
85
|
+
type TestCardDef = { id: string };
|
|
86
|
+
type TestCardMeta = Record<string, never>;
|
|
87
|
+
|
|
88
|
+
const internalState: InternalState<TestCardDef, TestCardMeta> = {
|
|
89
|
+
zones: {},
|
|
90
|
+
cards: {
|
|
91
|
+
"card-1": {
|
|
92
|
+
definitionId: "card-def-1",
|
|
93
|
+
owner: "player-1" as unknown as PlayerId,
|
|
94
|
+
controller: "player-1" as unknown as PlayerId,
|
|
95
|
+
zone: "deck" as unknown as ZoneId,
|
|
96
|
+
position: 0, // Top of deck
|
|
97
|
+
},
|
|
98
|
+
"card-2": {
|
|
99
|
+
definitionId: "card-def-2",
|
|
100
|
+
owner: "player-1" as unknown as PlayerId,
|
|
101
|
+
controller: "player-1" as unknown as PlayerId,
|
|
102
|
+
zone: "deck" as unknown as ZoneId,
|
|
103
|
+
position: 1,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
cardMetas: {},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
expect(internalState.cards["card-1"].position).toBe(0);
|
|
110
|
+
expect(internalState.cards["card-2"].position).toBe(1);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("IState", () => {
|
|
115
|
+
it("should wrap external game state with internal framework state", () => {
|
|
116
|
+
type GameState = {
|
|
117
|
+
turnCount: number;
|
|
118
|
+
currentPlayer: string;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type TestCardDef = { id: string };
|
|
122
|
+
type TestCardMeta = { damage?: number };
|
|
123
|
+
|
|
124
|
+
const state: IState<GameState, TestCardDef, TestCardMeta> = {
|
|
125
|
+
internal: {
|
|
126
|
+
zones: {},
|
|
127
|
+
cards: {},
|
|
128
|
+
cardMetas: {},
|
|
129
|
+
},
|
|
130
|
+
external: {
|
|
131
|
+
turnCount: 1,
|
|
132
|
+
currentPlayer: "player-1",
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Games can access their state
|
|
137
|
+
expect(state.external.turnCount).toBe(1);
|
|
138
|
+
expect(state.external.currentPlayer).toBe("player-1");
|
|
139
|
+
|
|
140
|
+
// Framework manages internal state
|
|
141
|
+
expect(state.internal.zones).toEqual({});
|
|
142
|
+
expect(state.internal.cards).toEqual({});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should allow complex external state while framework manages infrastructure", () => {
|
|
146
|
+
type GameState = {
|
|
147
|
+
players: Array<{ id: string; score: number }>;
|
|
148
|
+
effects: Array<{ type: string; duration: number }>;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
type TestCardDef = { id: string; name: string };
|
|
152
|
+
type TestCardMeta = { counters?: number };
|
|
153
|
+
|
|
154
|
+
const state: IState<GameState, TestCardDef, TestCardMeta> = {
|
|
155
|
+
internal: {
|
|
156
|
+
zones: {
|
|
157
|
+
hand: {
|
|
158
|
+
config: {
|
|
159
|
+
id: "hand" as unknown as ZoneId,
|
|
160
|
+
name: "Hand",
|
|
161
|
+
visibility: "private",
|
|
162
|
+
ordered: false,
|
|
163
|
+
},
|
|
164
|
+
cardIds: ["card-1"] as unknown as CardId[],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
cards: {
|
|
168
|
+
"card-1": {
|
|
169
|
+
definitionId: "monster-1",
|
|
170
|
+
owner: "player-1" as unknown as PlayerId,
|
|
171
|
+
controller: "player-1" as unknown as PlayerId,
|
|
172
|
+
zone: "hand" as unknown as ZoneId,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
cardMetas: {
|
|
176
|
+
"card-1": {
|
|
177
|
+
counters: 3,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
external: {
|
|
182
|
+
players: [
|
|
183
|
+
{ id: "player-1", score: 100 },
|
|
184
|
+
{ id: "player-2", score: 85 },
|
|
185
|
+
],
|
|
186
|
+
effects: [{ type: "global-buff", duration: 2 }],
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// External game logic
|
|
191
|
+
expect(state.external.players).toHaveLength(2);
|
|
192
|
+
expect(state.external.effects[0].type).toBe("global-buff");
|
|
193
|
+
|
|
194
|
+
// Internal framework management
|
|
195
|
+
expect(state.internal.zones.hand.cardIds).toContain("card-1");
|
|
196
|
+
expect(state.internal.cardMetas["card-1"].counters).toBe(3);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { CardZoneConfig } from "../zones";
|
|
2
|
+
import type { CardId, PlayerId, ZoneId } from "./index";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internal State - Managed by the Framework
|
|
6
|
+
*
|
|
7
|
+
* This state contains infrastructure concerns that the framework handles:
|
|
8
|
+
* - Zone management (which card is in which zone)
|
|
9
|
+
* - Card instance tracking (instance ID, owner, location)
|
|
10
|
+
* - Card metadata (dynamic properties, counters, effects)
|
|
11
|
+
*
|
|
12
|
+
* Games cannot directly modify internal state. They must use the operations API
|
|
13
|
+
* provided in move context.
|
|
14
|
+
*
|
|
15
|
+
* @template TCardDefinition - Static card definition type (game-specific)
|
|
16
|
+
* @template TCardMeta - Dynamic card metadata type (game-specific)
|
|
17
|
+
*/
|
|
18
|
+
export type InternalState<TCardDefinition = any, TCardMeta = any> = {
|
|
19
|
+
/**
|
|
20
|
+
* Zone registry - Maps zone ID to zone data
|
|
21
|
+
*
|
|
22
|
+
* Each zone contains:
|
|
23
|
+
* - config: Zone configuration (visibility, ordering, etc.)
|
|
24
|
+
* - cardIds: Array of card instance IDs in this zone
|
|
25
|
+
*
|
|
26
|
+
* The framework maintains this mapping and ensures consistency.
|
|
27
|
+
*/
|
|
28
|
+
zones: {
|
|
29
|
+
[zoneId: string]: {
|
|
30
|
+
config: CardZoneConfig;
|
|
31
|
+
cardIds: CardId[];
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Card instance registry - Maps card instance ID to card data
|
|
37
|
+
*
|
|
38
|
+
* Each card instance contains:
|
|
39
|
+
* - definitionId: Reference to the static card definition
|
|
40
|
+
* - owner: Player who owns this card (never changes)
|
|
41
|
+
* - controller: Player currently controlling this card (can change via effects)
|
|
42
|
+
* - zone: Current zone containing this card
|
|
43
|
+
* - position: Optional position in zone (for ordered zones like decks)
|
|
44
|
+
*
|
|
45
|
+
* Card instances are created during game setup or through game actions.
|
|
46
|
+
* Note: Field names align with CardInstanceBase from cards/card-instance.ts
|
|
47
|
+
*/
|
|
48
|
+
cards: {
|
|
49
|
+
[cardId: string]: {
|
|
50
|
+
/** Reference to card definition (static properties) */
|
|
51
|
+
definitionId: string;
|
|
52
|
+
/** Player who owns this card (never changes) */
|
|
53
|
+
owner: PlayerId;
|
|
54
|
+
/** Player currently controlling this card (can change via effects) */
|
|
55
|
+
controller: PlayerId;
|
|
56
|
+
/** Current zone containing this card */
|
|
57
|
+
zone: ZoneId;
|
|
58
|
+
/** Position in zone (for ordered zones) */
|
|
59
|
+
position?: number;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Card metadata registry - Maps card instance ID to dynamic metadata
|
|
65
|
+
*
|
|
66
|
+
* Stores mutable, game-specific card properties:
|
|
67
|
+
* - Damage/counters
|
|
68
|
+
* - Status effects (e.g., poisoned, stunned)
|
|
69
|
+
* - Gained abilities
|
|
70
|
+
* - Temporary modifications
|
|
71
|
+
*
|
|
72
|
+
* Metadata type is generic to allow game-specific structures.
|
|
73
|
+
*/
|
|
74
|
+
cardMetas: {
|
|
75
|
+
[cardId: string]: TCardMeta;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* On The Play (OTP) - Player who goes first
|
|
80
|
+
*
|
|
81
|
+
* Universal TCG concept. The OTP player typically goes first
|
|
82
|
+
* and may have different rules (e.g., no draw on first turn).
|
|
83
|
+
*/
|
|
84
|
+
otp?: PlayerId;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Choosing First Player - Player designated to choose who goes first
|
|
88
|
+
*
|
|
89
|
+
* In TCGs like Lorcana, one player is randomly selected to choose
|
|
90
|
+
* who will be the starting player (OTP). This field tracks which
|
|
91
|
+
* player has that privilege.
|
|
92
|
+
*
|
|
93
|
+
* Typically set during game initialization and cleared after OTP is chosen.
|
|
94
|
+
*/
|
|
95
|
+
choosingFirstPlayer?: PlayerId;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Players pending mulligan decision
|
|
99
|
+
*
|
|
100
|
+
* Tracks which players still need to decide whether to mulligan.
|
|
101
|
+
* Typically initialized with all players at game start.
|
|
102
|
+
*/
|
|
103
|
+
pendingMulligan?: PlayerId[];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Complete Game State
|
|
108
|
+
*
|
|
109
|
+
* Separates framework-managed state (internal) from game-specific state (external).
|
|
110
|
+
*
|
|
111
|
+
* - internal: Zone/card management handled by framework
|
|
112
|
+
* - external: Game-specific state defined by game developers
|
|
113
|
+
*
|
|
114
|
+
* Moves receive operations API to modify internal state.
|
|
115
|
+
* Moves receive Immer draft of external state for direct modification.
|
|
116
|
+
*
|
|
117
|
+
* @template TState - Game-specific state type
|
|
118
|
+
* @template TCardDefinition - Static card definition type
|
|
119
|
+
* @template TCardMeta - Dynamic card metadata type
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* type MyGameState = { turnCount: number; currentPlayer: string };
|
|
124
|
+
* type MyCardDef = { id: string; name: string; cost: number };
|
|
125
|
+
* type MyCardMeta = { damage?: number; effects?: string[] };
|
|
126
|
+
*
|
|
127
|
+
* const state: IState<MyGameState, MyCardDef, MyCardMeta> = {
|
|
128
|
+
* internal: {
|
|
129
|
+
* zones: { ... },
|
|
130
|
+
* cards: { ... },
|
|
131
|
+
* cardMetas: { ... }
|
|
132
|
+
* },
|
|
133
|
+
* external: {
|
|
134
|
+
* turnCount: 1,
|
|
135
|
+
* currentPlayer: "player-1"
|
|
136
|
+
* }
|
|
137
|
+
* };
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export type IState<TState, TCardDefinition = any, TCardMeta = any> = {
|
|
141
|
+
/**
|
|
142
|
+
* Framework-managed state
|
|
143
|
+
* Contains zone/card infrastructure
|
|
144
|
+
* Modified only through operations API
|
|
145
|
+
*/
|
|
146
|
+
internal: InternalState<TCardDefinition, TCardMeta>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Game-specific state
|
|
150
|
+
* Contains game logic state
|
|
151
|
+
* Modified directly via Immer draft in move reducers
|
|
152
|
+
*/
|
|
153
|
+
external: TState;
|
|
154
|
+
};
|