@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.
Files changed (157) hide show
  1. package/README.md +882 -0
  2. package/package.json +58 -0
  3. package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
  4. package/src/__tests__/createMockAlphaClashGame.ts +462 -0
  5. package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
  6. package/src/__tests__/createMockGundamGame.ts +379 -0
  7. package/src/__tests__/createMockLorcanaGame.ts +328 -0
  8. package/src/__tests__/createMockOnePieceGame.ts +429 -0
  9. package/src/__tests__/createMockRiftboundGame.ts +462 -0
  10. package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
  11. package/src/__tests__/gundam-engine-definition.test.ts +110 -0
  12. package/src/__tests__/integration-complete-game.test.ts +508 -0
  13. package/src/__tests__/integration-network-sync.test.ts +469 -0
  14. package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
  15. package/src/__tests__/move-enumeration.test.ts +725 -0
  16. package/src/__tests__/multiplayer-engine.test.ts +555 -0
  17. package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
  18. package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
  19. package/src/actions/action-definition.test.ts +201 -0
  20. package/src/actions/action-definition.ts +122 -0
  21. package/src/actions/action-timing.test.ts +490 -0
  22. package/src/actions/action-timing.ts +257 -0
  23. package/src/cards/card-definition.test.ts +268 -0
  24. package/src/cards/card-definition.ts +27 -0
  25. package/src/cards/card-instance.test.ts +422 -0
  26. package/src/cards/card-instance.ts +49 -0
  27. package/src/cards/computed-properties.test.ts +530 -0
  28. package/src/cards/computed-properties.ts +84 -0
  29. package/src/cards/conditional-modifiers.test.ts +390 -0
  30. package/src/cards/modifiers.test.ts +286 -0
  31. package/src/cards/modifiers.ts +51 -0
  32. package/src/engine/MULTIPLAYER.md +425 -0
  33. package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
  34. package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
  35. package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
  36. package/src/engine/__tests__/rule-engine.test.ts +366 -0
  37. package/src/engine/index.ts +14 -0
  38. package/src/engine/multiplayer-engine.example.ts +571 -0
  39. package/src/engine/multiplayer-engine.ts +409 -0
  40. package/src/engine/rule-engine.test.ts +286 -0
  41. package/src/engine/rule-engine.ts +1539 -0
  42. package/src/engine/tracker-system.ts +172 -0
  43. package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
  44. package/src/filtering/card-filter.test.ts +230 -0
  45. package/src/filtering/card-filter.ts +91 -0
  46. package/src/filtering/card-query.test.ts +901 -0
  47. package/src/filtering/card-query.ts +273 -0
  48. package/src/filtering/filter-matching.test.ts +944 -0
  49. package/src/filtering/filter-matching.ts +315 -0
  50. package/src/flow/SERIALIZATION.md +428 -0
  51. package/src/flow/__tests__/flow-definition.test.ts +427 -0
  52. package/src/flow/__tests__/flow-manager.test.ts +756 -0
  53. package/src/flow/__tests__/flow-serialization.test.ts +565 -0
  54. package/src/flow/flow-definition.ts +453 -0
  55. package/src/flow/flow-manager.ts +1044 -0
  56. package/src/flow/index.ts +35 -0
  57. package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
  58. package/src/game-definition/__tests__/game-definition.test.ts +291 -0
  59. package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
  60. package/src/game-definition/game-definition.ts +261 -0
  61. package/src/game-definition/index.ts +28 -0
  62. package/src/game-definition/move-definitions.ts +188 -0
  63. package/src/game-definition/validation.ts +183 -0
  64. package/src/history/history-manager.test.ts +497 -0
  65. package/src/history/history-manager.ts +312 -0
  66. package/src/history/history-operations.ts +122 -0
  67. package/src/history/index.ts +9 -0
  68. package/src/history/types.ts +255 -0
  69. package/src/index.ts +32 -0
  70. package/src/logging/index.ts +27 -0
  71. package/src/logging/log-formatter.ts +187 -0
  72. package/src/logging/logger.ts +276 -0
  73. package/src/logging/types.ts +148 -0
  74. package/src/moves/create-move.test.ts +331 -0
  75. package/src/moves/create-move.ts +64 -0
  76. package/src/moves/move-enumeration.ts +228 -0
  77. package/src/moves/move-executor.test.ts +431 -0
  78. package/src/moves/move-executor.ts +195 -0
  79. package/src/moves/move-system.test.ts +380 -0
  80. package/src/moves/move-system.ts +463 -0
  81. package/src/moves/standard-moves.ts +231 -0
  82. package/src/operations/card-operations.test.ts +236 -0
  83. package/src/operations/card-operations.ts +116 -0
  84. package/src/operations/card-registry-impl.test.ts +251 -0
  85. package/src/operations/card-registry-impl.ts +70 -0
  86. package/src/operations/card-registry.test.ts +234 -0
  87. package/src/operations/card-registry.ts +106 -0
  88. package/src/operations/counter-operations.ts +152 -0
  89. package/src/operations/game-operations.test.ts +280 -0
  90. package/src/operations/game-operations.ts +140 -0
  91. package/src/operations/index.ts +24 -0
  92. package/src/operations/operations-impl.test.ts +354 -0
  93. package/src/operations/operations-impl.ts +468 -0
  94. package/src/operations/zone-operations.test.ts +295 -0
  95. package/src/operations/zone-operations.ts +223 -0
  96. package/src/rng/seeded-rng.test.ts +339 -0
  97. package/src/rng/seeded-rng.ts +123 -0
  98. package/src/targeting/index.ts +48 -0
  99. package/src/targeting/target-definition.test.ts +273 -0
  100. package/src/targeting/target-definition.ts +37 -0
  101. package/src/targeting/target-dsl.ts +279 -0
  102. package/src/targeting/target-resolver.ts +486 -0
  103. package/src/targeting/target-validation.test.ts +994 -0
  104. package/src/targeting/target-validation.ts +286 -0
  105. package/src/telemetry/events.ts +202 -0
  106. package/src/telemetry/index.ts +21 -0
  107. package/src/telemetry/telemetry-manager.ts +127 -0
  108. package/src/telemetry/types.ts +68 -0
  109. package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
  110. package/src/testing/index.ts +88 -0
  111. package/src/testing/test-assertions.test.ts +341 -0
  112. package/src/testing/test-assertions.ts +256 -0
  113. package/src/testing/test-card-factory.test.ts +228 -0
  114. package/src/testing/test-card-factory.ts +111 -0
  115. package/src/testing/test-context-factory.ts +187 -0
  116. package/src/testing/test-end-assertions.test.ts +262 -0
  117. package/src/testing/test-end-assertions.ts +95 -0
  118. package/src/testing/test-engine-builder.test.ts +389 -0
  119. package/src/testing/test-engine-builder.ts +46 -0
  120. package/src/testing/test-flow-assertions.test.ts +284 -0
  121. package/src/testing/test-flow-assertions.ts +115 -0
  122. package/src/testing/test-player-builder.test.ts +132 -0
  123. package/src/testing/test-player-builder.ts +46 -0
  124. package/src/testing/test-replay-assertions.test.ts +356 -0
  125. package/src/testing/test-replay-assertions.ts +164 -0
  126. package/src/testing/test-rng-helpers.test.ts +260 -0
  127. package/src/testing/test-rng-helpers.ts +190 -0
  128. package/src/testing/test-state-builder.test.ts +373 -0
  129. package/src/testing/test-state-builder.ts +99 -0
  130. package/src/testing/test-zone-factory.test.ts +295 -0
  131. package/src/testing/test-zone-factory.ts +224 -0
  132. package/src/types/branded-utils.ts +54 -0
  133. package/src/types/branded.test.ts +175 -0
  134. package/src/types/branded.ts +33 -0
  135. package/src/types/index.ts +8 -0
  136. package/src/types/state.test.ts +198 -0
  137. package/src/types/state.ts +154 -0
  138. package/src/validation/card-type-guards.test.ts +242 -0
  139. package/src/validation/card-type-guards.ts +179 -0
  140. package/src/validation/index.ts +40 -0
  141. package/src/validation/schema-builders.test.ts +403 -0
  142. package/src/validation/schema-builders.ts +345 -0
  143. package/src/validation/type-guard-builder.test.ts +216 -0
  144. package/src/validation/type-guard-builder.ts +109 -0
  145. package/src/validation/validator-builder.test.ts +375 -0
  146. package/src/validation/validator-builder.ts +273 -0
  147. package/src/zones/index.ts +28 -0
  148. package/src/zones/zone-factory.test.ts +183 -0
  149. package/src/zones/zone-factory.ts +44 -0
  150. package/src/zones/zone-operations.test.ts +800 -0
  151. package/src/zones/zone-operations.ts +306 -0
  152. package/src/zones/zone-state-helpers.test.ts +337 -0
  153. package/src/zones/zone-state-helpers.ts +128 -0
  154. package/src/zones/zone-visibility.test.ts +156 -0
  155. package/src/zones/zone-visibility.ts +36 -0
  156. package/src/zones/zone.test.ts +186 -0
  157. package/src/zones/zone.ts +66 -0
@@ -0,0 +1,128 @@
1
+ import type { CardId, PlayerId } from "../types";
2
+ import type { Zone } from "./zone";
3
+ import { moveCard } from "./zone-operations";
4
+
5
+ /**
6
+ * Creates a record of zones for each player with optional initialization
7
+ * @param players - Array of player IDs
8
+ * @param initialValue - Optional factory function to create initial value for each player
9
+ * @returns Record mapping each player to their value
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // Create empty arrays for each player
14
+ * const hands = createPlayerZones(players);
15
+ *
16
+ * // Create with custom initial values
17
+ * const decks = createPlayerZones(players, () => []);
18
+ *
19
+ * // Create with complex initial values
20
+ * const zones = createPlayerZones(players, () => ({
21
+ * hand: [],
22
+ * deck: [],
23
+ * graveyard: []
24
+ * }));
25
+ * ```
26
+ */
27
+ export function createPlayerZones<T>(
28
+ players: PlayerId[],
29
+ initialValue?: () => T,
30
+ ): Record<PlayerId, T> {
31
+ const zones = {} as Record<PlayerId, T>;
32
+
33
+ for (const playerId of players) {
34
+ zones[playerId] = initialValue ? initialValue() : (undefined as T);
35
+ }
36
+
37
+ return zones;
38
+ }
39
+
40
+ /**
41
+ * Moves a card between zones in a flat zone state object
42
+ * @param state - Object containing zones as properties
43
+ * @param fromZoneKey - Key of the source zone in state
44
+ * @param toZoneKey - Key of the destination zone in state
45
+ * @param cardId - Card to move
46
+ * @param position - Optional position in destination zone
47
+ * @returns New state object with updated zones
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * const state = {
52
+ * hand: createZone(...),
53
+ * deck: createZone(...),
54
+ * };
55
+ *
56
+ * // Move card from hand to deck
57
+ * const newState = moveCardInState(state, "hand", "deck", cardId);
58
+ *
59
+ * // Move to specific position
60
+ * const newState2 = moveCardInState(state, "hand", "deck", cardId, 0);
61
+ * ```
62
+ */
63
+ export function moveCardInState<
64
+ TState extends Record<string, Zone>,
65
+ TFromKey extends keyof TState,
66
+ TToKey extends keyof TState,
67
+ >(
68
+ state: TState,
69
+ fromZoneKey: TFromKey,
70
+ toZoneKey: TToKey,
71
+ cardId: CardId,
72
+ position?: number,
73
+ ): TState {
74
+ const fromZone = state[fromZoneKey];
75
+ const toZone = state[toZoneKey];
76
+
77
+ if (!(fromZone && toZone)) {
78
+ throw new Error(
79
+ `Zone not found: ${fromZone ? String(toZoneKey) : String(fromZoneKey)}`,
80
+ );
81
+ }
82
+
83
+ const { fromZone: updatedFrom, toZone: updatedTo } = moveCard(
84
+ fromZone,
85
+ toZone,
86
+ cardId,
87
+ position,
88
+ );
89
+
90
+ return {
91
+ ...state,
92
+ [fromZoneKey]: updatedFrom,
93
+ [toZoneKey]: updatedTo,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Finds which zone contains a card in a flat zone state object
99
+ * @param state - Object containing zones as properties
100
+ * @param cardId - Card to find
101
+ * @returns Key of the zone containing the card, or undefined if not found
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * const state = {
106
+ * hand: createZone(..., [card1, card2]),
107
+ * deck: createZone(..., [card3]),
108
+ * };
109
+ *
110
+ * const zoneName = getCardZone(state, card1);
111
+ * // Returns: "hand"
112
+ *
113
+ * const missing = getCardZone(state, card4);
114
+ * // Returns: undefined
115
+ * ```
116
+ */
117
+ export function getCardZone<TState extends Record<string, Zone>>(
118
+ state: TState,
119
+ cardId: CardId,
120
+ ): keyof TState | undefined {
121
+ for (const key in state) {
122
+ const zone = state[key];
123
+ if (zone && zone.cards.includes(cardId)) {
124
+ return key;
125
+ }
126
+ }
127
+ return undefined;
128
+ }
@@ -0,0 +1,156 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createCardId, createPlayerId, createZoneId } from "../types";
3
+ import { createZone } from "./zone-factory";
4
+ import { filterZoneByVisibility } from "./zone-visibility";
5
+
6
+ describe("Zone Visibility", () => {
7
+ describe("filterZoneByVisibility", () => {
8
+ it("should show all cards in public zone to all players", () => {
9
+ const cards = [createCardId("card-1"), createCardId("card-2")];
10
+ const zone = createZone(
11
+ {
12
+ id: createZoneId("play"),
13
+ name: "Play Area",
14
+ visibility: "public",
15
+ ordered: false,
16
+ },
17
+ cards,
18
+ );
19
+
20
+ const viewerId = createPlayerId("player-1");
21
+ const filtered = filterZoneByVisibility(zone, viewerId);
22
+
23
+ expect(filtered.cards).toEqual(cards);
24
+ expect(filtered.cards).toHaveLength(2);
25
+ });
26
+
27
+ it("should show cards in private zone to owner", () => {
28
+ const ownerId = createPlayerId("player-1");
29
+ const cards = [createCardId("card-1"), createCardId("card-2")];
30
+ const zone = createZone(
31
+ {
32
+ id: createZoneId("hand"),
33
+ name: "Hand",
34
+ visibility: "private",
35
+ ordered: false,
36
+ owner: ownerId,
37
+ },
38
+ cards,
39
+ );
40
+
41
+ const filtered = filterZoneByVisibility(zone, ownerId);
42
+
43
+ expect(filtered.cards).toEqual(cards);
44
+ });
45
+
46
+ it("should hide cards in private zone from non-owner", () => {
47
+ const ownerId = createPlayerId("player-1");
48
+ const viewerId = createPlayerId("player-2");
49
+ const cards = [createCardId("card-1"), createCardId("card-2")];
50
+ const zone = createZone(
51
+ {
52
+ id: createZoneId("hand"),
53
+ name: "Hand",
54
+ visibility: "private",
55
+ ordered: false,
56
+ owner: ownerId,
57
+ },
58
+ cards,
59
+ );
60
+
61
+ const filtered = filterZoneByVisibility(zone, viewerId);
62
+
63
+ expect(filtered.cards).toHaveLength(0);
64
+ expect(filtered.config).toEqual(zone.config);
65
+ });
66
+
67
+ it("should hide all cards in secret zone from all players", () => {
68
+ const cards = [createCardId("card-1"), createCardId("card-2")];
69
+ const zone = createZone(
70
+ {
71
+ id: createZoneId("deck"),
72
+ name: "Deck",
73
+ visibility: "secret",
74
+ ordered: true,
75
+ faceDown: true,
76
+ },
77
+ cards,
78
+ );
79
+
80
+ const viewerId = createPlayerId("player-1");
81
+ const filtered = filterZoneByVisibility(zone, viewerId);
82
+
83
+ expect(filtered.cards).toHaveLength(0);
84
+ });
85
+
86
+ it("should show card count but hide contents for private zone", () => {
87
+ const ownerId = createPlayerId("player-1");
88
+ const viewerId = createPlayerId("player-2");
89
+ const cards = [
90
+ createCardId("card-1"),
91
+ createCardId("card-2"),
92
+ createCardId("card-3"),
93
+ ];
94
+ const zone = createZone(
95
+ {
96
+ id: createZoneId("hand"),
97
+ name: "Hand",
98
+ visibility: "private",
99
+ ordered: false,
100
+ owner: ownerId,
101
+ },
102
+ cards,
103
+ );
104
+
105
+ const filtered = filterZoneByVisibility(zone, viewerId);
106
+
107
+ // Config should be preserved so count can be determined
108
+ expect(filtered.config).toEqual(zone.config);
109
+ // But cards should be empty
110
+ expect(filtered.cards).toHaveLength(0);
111
+ });
112
+
113
+ it("should show card count but hide contents for secret zone", () => {
114
+ const cards = [
115
+ createCardId("card-1"),
116
+ createCardId("card-2"),
117
+ createCardId("card-3"),
118
+ ];
119
+ const zone = createZone(
120
+ {
121
+ id: createZoneId("deck"),
122
+ name: "Deck",
123
+ visibility: "secret",
124
+ ordered: true,
125
+ },
126
+ cards,
127
+ );
128
+
129
+ const viewerId = createPlayerId("player-1");
130
+ const filtered = filterZoneByVisibility(zone, viewerId);
131
+
132
+ // Config preserved
133
+ expect(filtered.config).toEqual(zone.config);
134
+ // Cards hidden
135
+ expect(filtered.cards).toHaveLength(0);
136
+ });
137
+
138
+ it("should handle zone without owner as public", () => {
139
+ const cards = [createCardId("card-1"), createCardId("card-2")];
140
+ const zone = createZone(
141
+ {
142
+ id: createZoneId("graveyard"),
143
+ name: "Graveyard",
144
+ visibility: "public",
145
+ ordered: true,
146
+ },
147
+ cards,
148
+ );
149
+
150
+ const viewerId = createPlayerId("player-1");
151
+ const filtered = filterZoneByVisibility(zone, viewerId);
152
+
153
+ expect(filtered.cards).toEqual(cards);
154
+ });
155
+ });
156
+ });
@@ -0,0 +1,36 @@
1
+ import { produce } from "immer";
2
+ import type { PlayerId } from "../types";
3
+ import type { Zone } from "./zone";
4
+
5
+ /**
6
+ * Filters a zone based on visibility rules and the viewing player
7
+ * @param zone - Zone to filter
8
+ * @param viewerId - ID of the player viewing the zone
9
+ * @returns Filtered zone with appropriate cards visible
10
+ */
11
+ export function filterZoneByVisibility(zone: Zone, viewerId: PlayerId): Zone {
12
+ // Public zones: everyone sees everything
13
+ if (zone.config.visibility === "public") {
14
+ return zone;
15
+ }
16
+
17
+ // Private zones: owner sees everything, others see nothing
18
+ if (zone.config.visibility === "private") {
19
+ if (zone.config.owner === viewerId) {
20
+ return zone;
21
+ }
22
+ // Non-owner sees config but no cards
23
+ return produce(zone, (draft) => {
24
+ draft.cards = [];
25
+ });
26
+ }
27
+
28
+ // Secret zones: no one sees card details
29
+ if (zone.config.visibility === "secret") {
30
+ return produce(zone, (draft) => {
31
+ draft.cards = [];
32
+ });
33
+ }
34
+
35
+ return zone;
36
+ }
@@ -0,0 +1,186 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { CardId, PlayerId, ZoneId } from "../types";
3
+ import type { CardZoneConfig, Zone, ZoneVisibility } from "./zone";
4
+
5
+ describe("Zone Type Definitions", () => {
6
+ describe("ZoneVisibility", () => {
7
+ it("should support public visibility type", () => {
8
+ const visibility: ZoneVisibility = "public";
9
+ expect(visibility).toBe("public");
10
+ });
11
+
12
+ it("should support private visibility type", () => {
13
+ const visibility: ZoneVisibility = "private";
14
+ expect(visibility).toBe("private");
15
+ });
16
+
17
+ it("should support secret visibility type", () => {
18
+ const visibility: ZoneVisibility = "secret";
19
+ expect(visibility).toBe("secret");
20
+ });
21
+ });
22
+
23
+ describe("ZoneConfig", () => {
24
+ it("should define valid zone configuration structure", () => {
25
+ const config: CardZoneConfig = {
26
+ id: "deck" as ZoneId,
27
+ name: "Deck",
28
+ visibility: "secret",
29
+ ordered: true,
30
+ faceDown: true,
31
+ };
32
+
33
+ expect(String(config.id)).toBe("deck");
34
+ expect(config.name).toBe("Deck");
35
+ expect(config.visibility).toBe("secret");
36
+ expect(config.ordered).toBe(true);
37
+ expect(config.faceDown).toBe(true);
38
+ });
39
+
40
+ it("should support optional owner property", () => {
41
+ const configWithOwner: CardZoneConfig = {
42
+ id: "hand" as ZoneId,
43
+ name: "Hand",
44
+ visibility: "private",
45
+ ordered: false,
46
+ owner: "player-1" as PlayerId,
47
+ };
48
+
49
+ expect(configWithOwner.owner).toBeDefined();
50
+ expect(typeof configWithOwner.owner).toBe("string");
51
+ });
52
+
53
+ it("should support optional maxSize property", () => {
54
+ const configWithMaxSize: CardZoneConfig = {
55
+ id: "hand" as ZoneId,
56
+ name: "Hand",
57
+ visibility: "private",
58
+ ordered: false,
59
+ maxSize: 7,
60
+ };
61
+
62
+ expect(configWithMaxSize.maxSize).toBe(7);
63
+ });
64
+
65
+ it("should work without optional properties", () => {
66
+ const minimalConfig: CardZoneConfig = {
67
+ id: "play" as ZoneId,
68
+ name: "Play Area",
69
+ visibility: "public",
70
+ ordered: false,
71
+ };
72
+
73
+ expect(minimalConfig.owner).toBeUndefined();
74
+ expect(minimalConfig.maxSize).toBeUndefined();
75
+ expect(minimalConfig.faceDown).toBeUndefined();
76
+ });
77
+ });
78
+
79
+ describe("Zone", () => {
80
+ it("should define valid zone structure with config and cards", () => {
81
+ const zone: Zone = {
82
+ config: {
83
+ id: "deck" as ZoneId,
84
+ name: "Deck",
85
+ visibility: "secret",
86
+ ordered: true,
87
+ faceDown: true,
88
+ },
89
+ cards: [],
90
+ };
91
+
92
+ expect(zone.config).toBeDefined();
93
+ expect(zone.cards).toBeInstanceOf(Array);
94
+ expect(zone.cards).toHaveLength(0);
95
+ });
96
+
97
+ it("should support zone with card IDs", () => {
98
+ const cardId1 = "card-1" as CardId;
99
+ const cardId2 = "card-2" as CardId;
100
+
101
+ const zone: Zone = {
102
+ config: {
103
+ id: "hand" as ZoneId,
104
+ name: "Hand",
105
+ visibility: "private",
106
+ ordered: false,
107
+ },
108
+ cards: [cardId1, cardId2],
109
+ };
110
+
111
+ expect(zone.cards).toHaveLength(2);
112
+ expect(zone.cards[0]).toBe(cardId1);
113
+ expect(zone.cards[1]).toBe(cardId2);
114
+ });
115
+
116
+ it("should maintain card order when ordered is true", () => {
117
+ const cardIds = [
118
+ "card-1" as CardId,
119
+ "card-2" as CardId,
120
+ "card-3" as CardId,
121
+ ];
122
+
123
+ const orderedZone: Zone = {
124
+ config: {
125
+ id: "deck" as ZoneId,
126
+ name: "Deck",
127
+ visibility: "secret",
128
+ ordered: true,
129
+ },
130
+ cards: cardIds,
131
+ };
132
+
133
+ expect(orderedZone.cards[0]).toBe(cardIds[0]);
134
+ expect(orderedZone.cards[1]).toBe(cardIds[1]);
135
+ expect(orderedZone.cards[2]).toBe(cardIds[2]);
136
+ });
137
+ });
138
+
139
+ describe("Zone Type Safety", () => {
140
+ it("should enforce ZoneId type for zone config id", () => {
141
+ const zoneId = "deck" as ZoneId;
142
+ const config: CardZoneConfig = {
143
+ id: zoneId,
144
+ name: "Deck",
145
+ visibility: "secret",
146
+ ordered: true,
147
+ };
148
+
149
+ const _typeCheck: ZoneId = config.id;
150
+ expect(config.id).toBe(zoneId);
151
+ });
152
+
153
+ it("should enforce CardId array type for zone cards", () => {
154
+ const cardIds: CardId[] = ["card-1" as CardId, "card-2" as CardId];
155
+
156
+ const zone: Zone = {
157
+ config: {
158
+ id: "hand" as ZoneId,
159
+ name: "Hand",
160
+ visibility: "private",
161
+ ordered: false,
162
+ },
163
+ cards: cardIds,
164
+ };
165
+
166
+ const _typeCheck: CardId[] = zone.cards;
167
+ expect(zone.cards).toEqual(cardIds);
168
+ });
169
+
170
+ it("should enforce PlayerId type for owner", () => {
171
+ const playerId = "player-1" as PlayerId;
172
+ const config: CardZoneConfig = {
173
+ id: "hand" as ZoneId,
174
+ name: "Hand",
175
+ visibility: "private",
176
+ ordered: false,
177
+ owner: playerId,
178
+ };
179
+
180
+ if (config.owner) {
181
+ const _typeCheck: PlayerId = config.owner;
182
+ expect(config.owner).toBe(playerId);
183
+ }
184
+ });
185
+ });
186
+ });
@@ -0,0 +1,66 @@
1
+ import type { CardId, PlayerId, ZoneId } from "../types";
2
+
3
+ /**
4
+ * Zone visibility types
5
+ *
6
+ * - public: All players can see all cards (e.g., play area, graveyard)
7
+ * - private: Owner can see cards, opponents see count (e.g., hand)
8
+ * - secret: No one can see cards, only count (e.g., deck, face-down cards)
9
+ */
10
+ export type ZoneVisibility = "public" | "private" | "secret";
11
+
12
+ /**
13
+ * Zone configuration defining the properties and rules of a zone
14
+ */
15
+ export type CardZoneConfig = {
16
+ /**
17
+ * Unique identifier for the zone
18
+ */
19
+ id: ZoneId;
20
+
21
+ /**
22
+ * Human-readable name of the zone
23
+ */
24
+ name: string;
25
+
26
+ /**
27
+ * Visibility rules for the zone
28
+ */
29
+ visibility: ZoneVisibility;
30
+
31
+ /**
32
+ * Whether card order matters in this zone
33
+ * True for decks, false for play areas
34
+ */
35
+ ordered: boolean;
36
+
37
+ /**
38
+ * Optional owner of the zone (undefined for shared zones)
39
+ */
40
+ owner?: PlayerId;
41
+
42
+ /**
43
+ * Whether cards are face-down by default
44
+ */
45
+ faceDown?: boolean;
46
+
47
+ /**
48
+ * Maximum number of cards allowed in the zone
49
+ */
50
+ maxSize?: number;
51
+ };
52
+
53
+ /**
54
+ * A zone containing cards with configuration
55
+ */
56
+ export type Zone = {
57
+ /**
58
+ * Zone configuration
59
+ */
60
+ config: CardZoneConfig;
61
+
62
+ /**
63
+ * Cards currently in the zone
64
+ */
65
+ cards: CardId[];
66
+ };