@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,555 @@
|
|
|
1
|
+
import { describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import type { Patch } from "immer";
|
|
3
|
+
import { MultiplayerEngine } from "../engine/multiplayer-engine";
|
|
4
|
+
import type { GameDefinition } from "../game-definition/game-definition";
|
|
5
|
+
import type { GameMoveDefinitions } from "../game-definition/move-definitions";
|
|
6
|
+
import { createPlayerId } from "../types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* MultiplayerEngine Tests
|
|
10
|
+
*
|
|
11
|
+
* Tests the multiplayer engine wrapper that encapsulates
|
|
12
|
+
* server-authoritative patterns for network gameplay.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type TestGameState = {
|
|
16
|
+
players: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
hand: string[];
|
|
20
|
+
score: number;
|
|
21
|
+
}>;
|
|
22
|
+
currentPlayerIndex: number;
|
|
23
|
+
deck: string[];
|
|
24
|
+
turnNumber: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type TestMoves = {
|
|
28
|
+
drawCard: Record<string, never>;
|
|
29
|
+
playCard: { cardId: string };
|
|
30
|
+
endTurn: Record<string, never>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("MultiplayerEngine", () => {
|
|
34
|
+
const createTestGame = (): GameDefinition<TestGameState, TestMoves> => {
|
|
35
|
+
const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
|
|
36
|
+
drawCard: {
|
|
37
|
+
reducer: (draft) => {
|
|
38
|
+
const card = draft.deck.pop();
|
|
39
|
+
if (card) {
|
|
40
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
41
|
+
if (player) {
|
|
42
|
+
player.hand.push(card);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
playCard: {
|
|
48
|
+
reducer: (draft, context) => {
|
|
49
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
50
|
+
if (player && context.params?.cardId) {
|
|
51
|
+
const cardId = context.params.cardId as string;
|
|
52
|
+
const cardIndex = player.hand.indexOf(cardId);
|
|
53
|
+
if (cardIndex >= 0) {
|
|
54
|
+
player.hand.splice(cardIndex, 1);
|
|
55
|
+
player.score += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
endTurn: {
|
|
61
|
+
reducer: (draft) => {
|
|
62
|
+
draft.currentPlayerIndex =
|
|
63
|
+
(draft.currentPlayerIndex + 1) % draft.players.length;
|
|
64
|
+
draft.turnNumber += 1;
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
name: "Test Game",
|
|
71
|
+
setup: (players) => ({
|
|
72
|
+
players: players.map((p) => ({
|
|
73
|
+
id: p.id,
|
|
74
|
+
name: p.name || "Player",
|
|
75
|
+
hand: [],
|
|
76
|
+
score: 0,
|
|
77
|
+
})),
|
|
78
|
+
currentPlayerIndex: 0,
|
|
79
|
+
deck: ["card1", "card2", "card3", "card4", "card5"],
|
|
80
|
+
turnNumber: 1,
|
|
81
|
+
}),
|
|
82
|
+
moves,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe("Server Mode", () => {
|
|
87
|
+
it("should create server-mode engine", () => {
|
|
88
|
+
const gameDefinition = createTestGame();
|
|
89
|
+
const players = [
|
|
90
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
91
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
95
|
+
mode: "server",
|
|
96
|
+
seed: "test-seed",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(server.getMode()).toBe("server");
|
|
100
|
+
expect(server.getState()).toBeDefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should execute moves and broadcast patches via callback", () => {
|
|
104
|
+
const gameDefinition = createTestGame();
|
|
105
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
106
|
+
|
|
107
|
+
const onPatchBroadcast = mock((broadcast) => {
|
|
108
|
+
expect(broadcast.patches).toBeDefined();
|
|
109
|
+
expect(broadcast.inversePatches).toBeDefined();
|
|
110
|
+
expect(broadcast.moveId).toBe("drawCard");
|
|
111
|
+
expect(broadcast.historyIndex).toBeGreaterThanOrEqual(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
115
|
+
mode: "server",
|
|
116
|
+
onPatchBroadcast,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = server.executeMove("drawCard", {
|
|
120
|
+
playerId: createPlayerId("p1"),
|
|
121
|
+
params: {},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.success).toBe(true);
|
|
125
|
+
expect(onPatchBroadcast).toHaveBeenCalledTimes(1);
|
|
126
|
+
|
|
127
|
+
// Verify state changed
|
|
128
|
+
const state = server.getState();
|
|
129
|
+
expect(state.players[0]?.hand.length).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("should call onMoveRejected callback for invalid moves", () => {
|
|
133
|
+
const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
|
|
134
|
+
drawCard: {
|
|
135
|
+
condition: () => false, // Always fails
|
|
136
|
+
reducer: () => {},
|
|
137
|
+
},
|
|
138
|
+
playCard: { reducer: () => {} },
|
|
139
|
+
endTurn: { reducer: () => {} },
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
143
|
+
name: "Test",
|
|
144
|
+
setup: createTestGame().setup,
|
|
145
|
+
moves,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
149
|
+
|
|
150
|
+
const onMoveRejected = mock((moveId, error, errorCode) => {
|
|
151
|
+
expect(moveId).toBe("drawCard");
|
|
152
|
+
expect(error).toContain("condition not met");
|
|
153
|
+
expect(errorCode).toBe("CONDITION_FAILED");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
157
|
+
mode: "server",
|
|
158
|
+
onMoveRejected,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const result = server.executeMove("drawCard", {
|
|
162
|
+
playerId: createPlayerId("p1"),
|
|
163
|
+
params: {},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(result.success).toBe(false);
|
|
167
|
+
expect(onMoveRejected).toHaveBeenCalledTimes(1);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should provide catchup patches for reconnecting clients", () => {
|
|
171
|
+
const gameDefinition = createTestGame();
|
|
172
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
173
|
+
|
|
174
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
175
|
+
mode: "server",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Execute 3 moves
|
|
179
|
+
server.executeMove("drawCard", {
|
|
180
|
+
playerId: createPlayerId("p1"),
|
|
181
|
+
params: {},
|
|
182
|
+
});
|
|
183
|
+
server.executeMove("playCard", {
|
|
184
|
+
playerId: createPlayerId("p1"),
|
|
185
|
+
params: { cardId: "card5" },
|
|
186
|
+
});
|
|
187
|
+
server.executeMove("endTurn", {
|
|
188
|
+
playerId: createPlayerId("p1"),
|
|
189
|
+
params: {},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Get all patches
|
|
193
|
+
const allPatches = server.getCatchupPatches(0);
|
|
194
|
+
expect(allPatches.length).toBeGreaterThan(0);
|
|
195
|
+
|
|
196
|
+
// Get patches from index 1
|
|
197
|
+
const partialPatches = server.getCatchupPatches(1);
|
|
198
|
+
expect(partialPatches.length).toBeLessThan(allPatches.length);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should manage client registration and tracking", () => {
|
|
202
|
+
const gameDefinition = createTestGame();
|
|
203
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
204
|
+
|
|
205
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
206
|
+
mode: "server",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Register clients
|
|
210
|
+
server.registerClient("client-1", -1);
|
|
211
|
+
server.registerClient("client-2", -1);
|
|
212
|
+
|
|
213
|
+
const clients = server.getAllClients();
|
|
214
|
+
expect(clients.length).toBe(2);
|
|
215
|
+
expect(clients[0]?.connected).toBe(true);
|
|
216
|
+
|
|
217
|
+
// Update sync index
|
|
218
|
+
server.updateClientSyncIndex("client-1", 5);
|
|
219
|
+
const client1 = server.getClientState("client-1");
|
|
220
|
+
expect(client1?.lastSyncedIndex).toBe(5);
|
|
221
|
+
|
|
222
|
+
// Unregister client
|
|
223
|
+
server.unregisterClient("client-1");
|
|
224
|
+
const disconnectedClient = server.getClientState("client-1");
|
|
225
|
+
expect(disconnectedClient?.connected).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should provide current history index", () => {
|
|
229
|
+
const gameDefinition = createTestGame();
|
|
230
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
231
|
+
|
|
232
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
233
|
+
mode: "server",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(server.getCurrentHistoryIndex()).toBe(-1); // No moves yet
|
|
237
|
+
|
|
238
|
+
server.executeMove("drawCard", {
|
|
239
|
+
playerId: createPlayerId("p1"),
|
|
240
|
+
params: {},
|
|
241
|
+
});
|
|
242
|
+
expect(server.getCurrentHistoryIndex()).toBe(0);
|
|
243
|
+
|
|
244
|
+
server.executeMove("endTurn", {
|
|
245
|
+
playerId: createPlayerId("p1"),
|
|
246
|
+
params: {},
|
|
247
|
+
});
|
|
248
|
+
expect(server.getCurrentHistoryIndex()).toBe(1);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should throw error when client tries server-only operations", () => {
|
|
252
|
+
const gameDefinition = createTestGame();
|
|
253
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
254
|
+
|
|
255
|
+
const client = new MultiplayerEngine(gameDefinition, players, {
|
|
256
|
+
mode: "client",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(() =>
|
|
260
|
+
client.executeMove("drawCard", {
|
|
261
|
+
playerId: createPlayerId("p1"),
|
|
262
|
+
params: {},
|
|
263
|
+
}),
|
|
264
|
+
).toThrow("Only server can execute moves");
|
|
265
|
+
|
|
266
|
+
expect(() => client.getCatchupPatches()).toThrow(
|
|
267
|
+
"Only server can provide catchup patches",
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(() => client.registerClient("test")).toThrow(
|
|
271
|
+
"Only server can register clients",
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(() => client.getHistory()).toThrow(
|
|
275
|
+
"Only server maintains authoritative history",
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("Client Mode", () => {
|
|
281
|
+
it("should create client-mode engine", () => {
|
|
282
|
+
const gameDefinition = createTestGame();
|
|
283
|
+
const players = [
|
|
284
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
285
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
const client = new MultiplayerEngine(gameDefinition, players, {
|
|
289
|
+
mode: "client",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(client.getMode()).toBe("client");
|
|
293
|
+
expect(client.getState()).toBeDefined();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should apply patches from server", () => {
|
|
297
|
+
const gameDefinition = createTestGame();
|
|
298
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
299
|
+
|
|
300
|
+
const onPatchesApplied = mock((patches: Patch[]) => {
|
|
301
|
+
expect(patches.length).toBeGreaterThan(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const client = new MultiplayerEngine(gameDefinition, players, {
|
|
305
|
+
mode: "client",
|
|
306
|
+
onPatchesApplied,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Simulate receiving patches from server
|
|
310
|
+
// Create patches by executing on a server engine
|
|
311
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
312
|
+
mode: "server",
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const result = server.executeMove("drawCard", {
|
|
316
|
+
playerId: createPlayerId("p1"),
|
|
317
|
+
params: {},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (result.success) {
|
|
321
|
+
// Client applies patches
|
|
322
|
+
client.applyServerPatches(result.patches);
|
|
323
|
+
|
|
324
|
+
expect(onPatchesApplied).toHaveBeenCalledTimes(1);
|
|
325
|
+
|
|
326
|
+
// States should match
|
|
327
|
+
expect(client.getState()).toEqual(server.getState());
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("should throw error when server tries client-only operations", () => {
|
|
332
|
+
const gameDefinition = createTestGame();
|
|
333
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
334
|
+
|
|
335
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
336
|
+
mode: "server",
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
expect(() => server.applyServerPatches([])).toThrow(
|
|
340
|
+
"Only clients can apply server patches",
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("Server-Client Synchronization", () => {
|
|
346
|
+
it("should keep server and clients in sync through patches", () => {
|
|
347
|
+
const gameDefinition = createTestGame();
|
|
348
|
+
const players = [
|
|
349
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
350
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
// Create server
|
|
354
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
355
|
+
mode: "server",
|
|
356
|
+
seed: "test-seed",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Create clients
|
|
360
|
+
const client1 = new MultiplayerEngine(gameDefinition, players, {
|
|
361
|
+
mode: "client",
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const client2 = new MultiplayerEngine(gameDefinition, players, {
|
|
365
|
+
mode: "client",
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Initial states match
|
|
369
|
+
expect(server.getState()).toEqual(client1.getState());
|
|
370
|
+
expect(server.getState()).toEqual(client2.getState());
|
|
371
|
+
|
|
372
|
+
// Execute move on server
|
|
373
|
+
const result = server.executeMove("drawCard", {
|
|
374
|
+
playerId: createPlayerId("p1"),
|
|
375
|
+
params: {},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (result.success) {
|
|
379
|
+
// Broadcast to clients
|
|
380
|
+
client1.applyServerPatches(result.patches);
|
|
381
|
+
client2.applyServerPatches(result.patches);
|
|
382
|
+
|
|
383
|
+
// All states match
|
|
384
|
+
expect(server.getState()).toEqual(client1.getState());
|
|
385
|
+
expect(server.getState()).toEqual(client2.getState());
|
|
386
|
+
|
|
387
|
+
// Verify move executed
|
|
388
|
+
expect(server.getState().players[0]?.hand.length).toBe(1);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("should handle multiple sequential moves with synchronization", () => {
|
|
393
|
+
const gameDefinition = createTestGame();
|
|
394
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
395
|
+
|
|
396
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
397
|
+
mode: "server",
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const client = new MultiplayerEngine(gameDefinition, players, {
|
|
401
|
+
mode: "client",
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const moves = [
|
|
405
|
+
{ move: "drawCard", params: {} },
|
|
406
|
+
{ move: "playCard", params: { cardId: "card5" } },
|
|
407
|
+
{ move: "endTurn", params: {} },
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
for (const moveData of moves) {
|
|
411
|
+
const result = server.executeMove(moveData.move, {
|
|
412
|
+
playerId: createPlayerId("p1"),
|
|
413
|
+
params: moveData.params,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (result.success) {
|
|
417
|
+
client.applyServerPatches(result.patches);
|
|
418
|
+
expect(server.getState()).toEqual(client.getState());
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Final state verification
|
|
423
|
+
const finalState = server.getState();
|
|
424
|
+
expect(finalState.players[0]?.hand.length).toBe(0);
|
|
425
|
+
expect(finalState.players[0]?.score).toBe(1);
|
|
426
|
+
expect(finalState.turnNumber).toBe(2);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("should handle client reconnection with batch patches", () => {
|
|
430
|
+
const gameDefinition = createTestGame();
|
|
431
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
432
|
+
|
|
433
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
434
|
+
mode: "server",
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Execute moves while client is disconnected
|
|
438
|
+
server.executeMove("drawCard", {
|
|
439
|
+
playerId: createPlayerId("p1"),
|
|
440
|
+
params: {},
|
|
441
|
+
});
|
|
442
|
+
server.executeMove("endTurn", {
|
|
443
|
+
playerId: createPlayerId("p1"),
|
|
444
|
+
params: {},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Client reconnects
|
|
448
|
+
const reconnectedClient = new MultiplayerEngine(gameDefinition, players, {
|
|
449
|
+
mode: "client",
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Get all patches and apply
|
|
453
|
+
const allPatches = server.getCatchupPatches();
|
|
454
|
+
reconnectedClient.applyServerPatches(allPatches);
|
|
455
|
+
|
|
456
|
+
// Client is synced
|
|
457
|
+
expect(reconnectedClient.getState()).toEqual(server.getState());
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe("Common Operations", () => {
|
|
462
|
+
it("should support getState on both server and client", () => {
|
|
463
|
+
const gameDefinition = createTestGame();
|
|
464
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
465
|
+
|
|
466
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
467
|
+
mode: "server",
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const client = new MultiplayerEngine(gameDefinition, players, {
|
|
471
|
+
mode: "client",
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
expect(server.getState()).toBeDefined();
|
|
475
|
+
expect(client.getState()).toBeDefined();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("should support getPlayerView on both server and client", () => {
|
|
479
|
+
const gameDefinition = createTestGame();
|
|
480
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
481
|
+
|
|
482
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
483
|
+
mode: "server",
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const client = new MultiplayerEngine(gameDefinition, players, {
|
|
487
|
+
mode: "client",
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const serverView = server.getPlayerView(createPlayerId("p1"));
|
|
491
|
+
const clientView = client.getPlayerView(createPlayerId("p1"));
|
|
492
|
+
|
|
493
|
+
expect(serverView).toBeDefined();
|
|
494
|
+
expect(clientView).toBeDefined();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("should support canExecuteMove on both server and client", () => {
|
|
498
|
+
const gameDefinition = createTestGame();
|
|
499
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
500
|
+
|
|
501
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
502
|
+
mode: "server",
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const client = new MultiplayerEngine(gameDefinition, players, {
|
|
506
|
+
mode: "client",
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const canExecuteServer = server.canExecuteMove("drawCard", {
|
|
510
|
+
playerId: createPlayerId("p1"),
|
|
511
|
+
params: {},
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const canExecuteClient = client.canExecuteMove("drawCard", {
|
|
515
|
+
playerId: createPlayerId("p1"),
|
|
516
|
+
params: {},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
expect(canExecuteServer).toBe(true);
|
|
520
|
+
expect(canExecuteClient).toBe(true);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("should support getValidMoves on both server and client", () => {
|
|
524
|
+
const gameDefinition = createTestGame();
|
|
525
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
526
|
+
|
|
527
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
528
|
+
mode: "server",
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const client = new MultiplayerEngine(gameDefinition, players, {
|
|
532
|
+
mode: "client",
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const serverMoves = server.getValidMoves(createPlayerId("p1"));
|
|
536
|
+
const clientMoves = client.getValidMoves(createPlayerId("p1"));
|
|
537
|
+
|
|
538
|
+
expect(serverMoves.length).toBeGreaterThan(0);
|
|
539
|
+
expect(clientMoves.length).toBeGreaterThan(0);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("should provide access to underlying engine", () => {
|
|
543
|
+
const gameDefinition = createTestGame();
|
|
544
|
+
const players = [{ id: createPlayerId("p1"), name: "Alice" }];
|
|
545
|
+
|
|
546
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
547
|
+
mode: "server",
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const engine = server.getEngine();
|
|
551
|
+
expect(engine).toBeDefined();
|
|
552
|
+
expect(engine.getState).toBeDefined();
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createTestEngine } from "../testing/test-engine-builder";
|
|
3
|
+
import { createTestPlayers } from "../testing/test-player-builder";
|
|
4
|
+
import { createMockOnePieceGame } from "./createMockOnePieceGame";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One Piece Card Game - Engine Feature Tests
|
|
8
|
+
*
|
|
9
|
+
* Refactored to showcase:
|
|
10
|
+
* ✅ High-level zone utilities (createDeck, drawCards, mulligan, bulkMove)
|
|
11
|
+
* ✅ Flow context access (isFirstTurn, turn)
|
|
12
|
+
* ✅ Standard moves (pass, concede)
|
|
13
|
+
* ✅ Massive simplification (10 fields → 2 fields, -80%)
|
|
14
|
+
*/
|
|
15
|
+
describe("One Piece Game - Refactored Engine Features", () => {
|
|
16
|
+
it("should initialize game with ONLY game-specific state", () => {
|
|
17
|
+
const gameDefinition = createMockOnePieceGame();
|
|
18
|
+
const players = createTestPlayers(2);
|
|
19
|
+
const engine = createTestEngine(gameDefinition, players);
|
|
20
|
+
|
|
21
|
+
const state = engine.getState();
|
|
22
|
+
|
|
23
|
+
// ✅ NEW: Only game-specific data
|
|
24
|
+
expect(state.battleAllowed).toBe(false);
|
|
25
|
+
expect(state.leaderLife).toBeDefined();
|
|
26
|
+
|
|
27
|
+
// ✅ REMOVED: Massive reduction
|
|
28
|
+
// @ts-expect-error
|
|
29
|
+
expect(state.phase).toBeUndefined();
|
|
30
|
+
// @ts-expect-error
|
|
31
|
+
expect(state.setupStep).toBeUndefined();
|
|
32
|
+
// @ts-expect-error
|
|
33
|
+
expect(state.turn).toBeUndefined();
|
|
34
|
+
// @ts-expect-error
|
|
35
|
+
expect(state.currentPlayer).toBeUndefined();
|
|
36
|
+
// @ts-expect-error
|
|
37
|
+
expect(state.firstTurn).toBeUndefined();
|
|
38
|
+
// @ts-expect-error
|
|
39
|
+
expect(state.mulliganOffered).toBeUndefined();
|
|
40
|
+
// @ts-expect-error
|
|
41
|
+
expect(state.donThisTurn).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should have proper zone configuration", () => {
|
|
45
|
+
const gameDefinition = createMockOnePieceGame();
|
|
46
|
+
const zones = gameDefinition.zones;
|
|
47
|
+
|
|
48
|
+
// Verify One Piece zones
|
|
49
|
+
expect(zones?.deck).toBeDefined();
|
|
50
|
+
expect(zones?.hand).toBeDefined();
|
|
51
|
+
expect(zones?.donDeck).toBeDefined();
|
|
52
|
+
expect(zones?.donArea).toBeDefined();
|
|
53
|
+
expect(zones?.leader).toBeDefined();
|
|
54
|
+
expect(zones?.characters).toBeDefined();
|
|
55
|
+
expect(zones?.stage).toBeDefined();
|
|
56
|
+
expect(zones?.life).toBeDefined();
|
|
57
|
+
expect(zones?.discard).toBeDefined();
|
|
58
|
+
|
|
59
|
+
expect(zones?.deck?.maxSize).toBe(50);
|
|
60
|
+
expect(zones?.donDeck?.maxSize).toBe(10);
|
|
61
|
+
expect(zones?.leader?.maxSize).toBe(1);
|
|
62
|
+
expect(zones?.life?.maxSize).toBe(5);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should use ALL high-level zone utilities", () => {
|
|
66
|
+
// ✅ One Piece uses ALL 4 utilities!
|
|
67
|
+
// - createDeck() for deck initialization
|
|
68
|
+
// - drawCards() for drawing
|
|
69
|
+
// - mulligan() for redraw
|
|
70
|
+
// - bulkMove() for life card placement
|
|
71
|
+
|
|
72
|
+
const gameDefinition = createMockOnePieceGame();
|
|
73
|
+
expect(gameDefinition.moves.initializeDecks).toBeDefined();
|
|
74
|
+
expect(gameDefinition.moves.drawOpeningHand).toBeDefined();
|
|
75
|
+
expect(gameDefinition.moves.decideMulligan).toBeDefined();
|
|
76
|
+
expect(gameDefinition.moves.placeLifeCards).toBeDefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should use flow context for first turn draw skip", () => {
|
|
80
|
+
const gameDefinition = createMockOnePieceGame();
|
|
81
|
+
|
|
82
|
+
// draw move uses context.flow.isFirstTurn and context.flow.currentPlayer
|
|
83
|
+
const draw = gameDefinition.moves.draw;
|
|
84
|
+
expect(draw.condition).toBeDefined();
|
|
85
|
+
|
|
86
|
+
// First player skips draw on first turn
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should use flow context for DON!! placement", () => {
|
|
90
|
+
const gameDefinition = createMockOnePieceGame();
|
|
91
|
+
|
|
92
|
+
// placeDon uses context.flow.turn to determine DON!! count
|
|
93
|
+
const placeDon = gameDefinition.moves.placeDon;
|
|
94
|
+
expect(placeDon.reducer).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should use standard moves", () => {
|
|
98
|
+
const gameDefinition = createMockOnePieceGame();
|
|
99
|
+
|
|
100
|
+
expect(gameDefinition.moves.pass).toBeDefined();
|
|
101
|
+
expect(gameDefinition.moves.concede).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should demonstrate largest boilerplate reduction", () => {
|
|
105
|
+
// State fields: 10 → 2 (-80%)
|
|
106
|
+
// 593 lines → 430 lines (-27% - HIGHEST reduction!)
|
|
107
|
+
|
|
108
|
+
const gameDefinition = createMockOnePieceGame();
|
|
109
|
+
const players = createTestPlayers(2);
|
|
110
|
+
const state = gameDefinition.setup(players);
|
|
111
|
+
|
|
112
|
+
expect(Object.keys(state).length).toBe(2);
|
|
113
|
+
});
|
|
114
|
+
});
|