@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,389 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { GameDefinition } from "../game-definition/game-definition";
|
|
3
|
+
import type { GameMoveDefinitions } from "../game-definition/move-definitions";
|
|
4
|
+
import { createPlayerId } from "../types";
|
|
5
|
+
import { createTestEngine } from "./test-engine-builder";
|
|
6
|
+
import { createTestPlayers } from "./test-player-builder";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tests for createTestEngine - Engine builder for tests
|
|
10
|
+
*
|
|
11
|
+
* Task 2.1: Write tests for test builders (createTestEngine)
|
|
12
|
+
*
|
|
13
|
+
* Tests verify:
|
|
14
|
+
* - Creating engine with minimal game definition
|
|
15
|
+
* - Creating engine with custom players
|
|
16
|
+
* - Creating engine with options (seed)
|
|
17
|
+
* - Default player generation
|
|
18
|
+
* - Engine is properly initialized and functional
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
type SimpleGameState = {
|
|
22
|
+
players: Array<{ id: string; name: string; score: number }>;
|
|
23
|
+
currentPlayer: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type SimpleMoves = {
|
|
27
|
+
incrementScore: Record<string, never>;
|
|
28
|
+
pass: Record<string, never>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("createTestEngine", () => {
|
|
32
|
+
describe("Basic Functionality", () => {
|
|
33
|
+
it("should create engine with minimal game definition and default players", () => {
|
|
34
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
35
|
+
incrementScore: {
|
|
36
|
+
reducer: (draft: SimpleGameState) => {
|
|
37
|
+
const player = draft.players[draft.currentPlayer];
|
|
38
|
+
if (player) {
|
|
39
|
+
player.score += 1;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
pass: {
|
|
44
|
+
reducer: (draft: SimpleGameState) => {
|
|
45
|
+
draft.currentPlayer =
|
|
46
|
+
(draft.currentPlayer + 1) % draft.players.length;
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
52
|
+
name: "Test Game",
|
|
53
|
+
setup: (players) => ({
|
|
54
|
+
players: players.map((p) => ({
|
|
55
|
+
id: p.id,
|
|
56
|
+
name: p.name || "Player",
|
|
57
|
+
score: 0,
|
|
58
|
+
})),
|
|
59
|
+
currentPlayer: 0,
|
|
60
|
+
}),
|
|
61
|
+
moves,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const engine = createTestEngine(definition);
|
|
65
|
+
|
|
66
|
+
// Engine should be initialized
|
|
67
|
+
expect(engine).toBeDefined();
|
|
68
|
+
|
|
69
|
+
// Should have default 2 players
|
|
70
|
+
const state = engine.getState();
|
|
71
|
+
expect(state.players).toHaveLength(2);
|
|
72
|
+
expect(state.players[0]?.name).toBe("Player 1");
|
|
73
|
+
expect(state.players[1]?.name).toBe("Player 2");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should create engine with custom players", () => {
|
|
77
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
78
|
+
incrementScore: { reducer: () => {} },
|
|
79
|
+
pass: { reducer: () => {} },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
83
|
+
name: "Test Game",
|
|
84
|
+
setup: (players) => ({
|
|
85
|
+
players: players.map((p) => ({
|
|
86
|
+
id: p.id,
|
|
87
|
+
name: p.name || "Player",
|
|
88
|
+
score: 0,
|
|
89
|
+
})),
|
|
90
|
+
currentPlayer: 0,
|
|
91
|
+
}),
|
|
92
|
+
moves,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const customPlayers = createTestPlayers(3, ["Alice", "Bob", "Charlie"]);
|
|
96
|
+
const engine = createTestEngine(definition, customPlayers);
|
|
97
|
+
|
|
98
|
+
const state = engine.getState();
|
|
99
|
+
expect(state.players).toHaveLength(3);
|
|
100
|
+
expect(state.players[0]?.name).toBe("Alice");
|
|
101
|
+
expect(state.players[1]?.name).toBe("Bob");
|
|
102
|
+
expect(state.players[2]?.name).toBe("Charlie");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should create engine with seed option for deterministic RNG", () => {
|
|
106
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
107
|
+
incrementScore: { reducer: () => {} },
|
|
108
|
+
pass: { reducer: () => {} },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
112
|
+
name: "Test Game",
|
|
113
|
+
setup: (players) => ({
|
|
114
|
+
players: players.map((p) => ({
|
|
115
|
+
id: p.id,
|
|
116
|
+
name: p.name || "Player",
|
|
117
|
+
score: 0,
|
|
118
|
+
})),
|
|
119
|
+
currentPlayer: 0,
|
|
120
|
+
}),
|
|
121
|
+
moves,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const engine = createTestEngine(definition, undefined, {
|
|
125
|
+
seed: "test-seed",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Engine should have seeded RNG
|
|
129
|
+
expect(engine.getRNG()).toBeDefined();
|
|
130
|
+
expect(engine.getRNG().getSeed()).toBe("test-seed");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("Engine Functionality", () => {
|
|
135
|
+
it("should create functional engine that can execute moves", () => {
|
|
136
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
137
|
+
incrementScore: {
|
|
138
|
+
reducer: (draft: SimpleGameState) => {
|
|
139
|
+
const player = draft.players[draft.currentPlayer];
|
|
140
|
+
if (player) {
|
|
141
|
+
player.score += 1;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
pass: { reducer: () => {} },
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
149
|
+
name: "Test Game",
|
|
150
|
+
setup: (players) => ({
|
|
151
|
+
players: players.map((p) => ({
|
|
152
|
+
id: p.id,
|
|
153
|
+
name: p.name || "Player",
|
|
154
|
+
score: 0,
|
|
155
|
+
})),
|
|
156
|
+
currentPlayer: 0,
|
|
157
|
+
}),
|
|
158
|
+
moves,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const engine = createTestEngine(definition);
|
|
162
|
+
|
|
163
|
+
const result = engine.executeMove("incrementScore", {
|
|
164
|
+
playerId: createPlayerId("test-p1"),
|
|
165
|
+
params: {},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result.success).toBe(true);
|
|
169
|
+
|
|
170
|
+
const state = engine.getState();
|
|
171
|
+
expect(state.players[0]?.score).toBe(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should create engine with history tracking", () => {
|
|
175
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
176
|
+
incrementScore: {
|
|
177
|
+
reducer: (draft: SimpleGameState) => {
|
|
178
|
+
const player = draft.players[draft.currentPlayer];
|
|
179
|
+
if (player) {
|
|
180
|
+
player.score += 1;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
pass: { reducer: () => {} },
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
188
|
+
name: "Test Game",
|
|
189
|
+
setup: (players) => ({
|
|
190
|
+
players: players.map((p) => ({
|
|
191
|
+
id: p.id,
|
|
192
|
+
name: p.name || "Player",
|
|
193
|
+
score: 0,
|
|
194
|
+
})),
|
|
195
|
+
currentPlayer: 0,
|
|
196
|
+
}),
|
|
197
|
+
moves,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const engine = createTestEngine(definition);
|
|
201
|
+
|
|
202
|
+
engine.executeMove("incrementScore", {
|
|
203
|
+
playerId: createPlayerId("test-p1"),
|
|
204
|
+
params: {},
|
|
205
|
+
});
|
|
206
|
+
engine.executeMove("incrementScore", {
|
|
207
|
+
playerId: createPlayerId("test-p1"),
|
|
208
|
+
params: {},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const history = engine.getHistory();
|
|
212
|
+
expect(history.length).toBe(2);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should create engine that supports undo/redo", () => {
|
|
216
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
217
|
+
incrementScore: {
|
|
218
|
+
reducer: (draft: SimpleGameState) => {
|
|
219
|
+
const player = draft.players[draft.currentPlayer];
|
|
220
|
+
if (player) {
|
|
221
|
+
player.score += 1;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
pass: { reducer: () => {} },
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
229
|
+
name: "Test Game",
|
|
230
|
+
setup: (players) => ({
|
|
231
|
+
players: players.map((p) => ({
|
|
232
|
+
id: p.id,
|
|
233
|
+
name: p.name || "Player",
|
|
234
|
+
score: 0,
|
|
235
|
+
})),
|
|
236
|
+
currentPlayer: 0,
|
|
237
|
+
}),
|
|
238
|
+
moves,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const engine = createTestEngine(definition);
|
|
242
|
+
|
|
243
|
+
engine.executeMove("incrementScore", {
|
|
244
|
+
playerId: createPlayerId("test-p1"),
|
|
245
|
+
params: {},
|
|
246
|
+
});
|
|
247
|
+
expect(engine.getState().players[0]?.score).toBe(1);
|
|
248
|
+
|
|
249
|
+
engine.undo();
|
|
250
|
+
expect(engine.getState().players[0]?.score).toBe(0);
|
|
251
|
+
|
|
252
|
+
engine.redo();
|
|
253
|
+
expect(engine.getState().players[0]?.score).toBe(1);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("Options", () => {
|
|
258
|
+
it("should accept undefined players and use defaults", () => {
|
|
259
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
260
|
+
incrementScore: { reducer: () => {} },
|
|
261
|
+
pass: { reducer: () => {} },
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
265
|
+
name: "Test Game",
|
|
266
|
+
setup: (players) => ({
|
|
267
|
+
players: players.map((p) => ({
|
|
268
|
+
id: p.id,
|
|
269
|
+
name: p.name || "Player",
|
|
270
|
+
score: 0,
|
|
271
|
+
})),
|
|
272
|
+
currentPlayer: 0,
|
|
273
|
+
}),
|
|
274
|
+
moves,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const engine = createTestEngine(definition, undefined);
|
|
278
|
+
|
|
279
|
+
const state = engine.getState();
|
|
280
|
+
expect(state.players).toHaveLength(2);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should accept undefined options", () => {
|
|
284
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
285
|
+
incrementScore: { reducer: () => {} },
|
|
286
|
+
pass: { reducer: () => {} },
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
290
|
+
name: "Test Game",
|
|
291
|
+
setup: (players) => ({
|
|
292
|
+
players: players.map((p) => ({
|
|
293
|
+
id: p.id,
|
|
294
|
+
name: p.name || "Player",
|
|
295
|
+
score: 0,
|
|
296
|
+
})),
|
|
297
|
+
currentPlayer: 0,
|
|
298
|
+
}),
|
|
299
|
+
moves,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const engine = createTestEngine(definition, undefined, undefined);
|
|
303
|
+
|
|
304
|
+
expect(engine).toBeDefined();
|
|
305
|
+
expect(engine.getRNG()).toBeDefined();
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("should merge custom options with defaults", () => {
|
|
309
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
310
|
+
incrementScore: { reducer: () => {} },
|
|
311
|
+
pass: { reducer: () => {} },
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
315
|
+
name: "Test Game",
|
|
316
|
+
setup: (players) => ({
|
|
317
|
+
players: players.map((p) => ({
|
|
318
|
+
id: p.id,
|
|
319
|
+
name: p.name || "Player",
|
|
320
|
+
score: 0,
|
|
321
|
+
})),
|
|
322
|
+
currentPlayer: 0,
|
|
323
|
+
}),
|
|
324
|
+
moves,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const engine = createTestEngine(definition, undefined, {
|
|
328
|
+
seed: "custom-seed",
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(engine.getRNG().getSeed()).toBe("custom-seed");
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("Edge Cases", () => {
|
|
336
|
+
it("should create engine with single player", () => {
|
|
337
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
338
|
+
incrementScore: { reducer: () => {} },
|
|
339
|
+
pass: { reducer: () => {} },
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
343
|
+
name: "Test Game",
|
|
344
|
+
setup: (players) => ({
|
|
345
|
+
players: players.map((p) => ({
|
|
346
|
+
id: p.id,
|
|
347
|
+
name: p.name || "Player",
|
|
348
|
+
score: 0,
|
|
349
|
+
})),
|
|
350
|
+
currentPlayer: 0,
|
|
351
|
+
}),
|
|
352
|
+
moves,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const singlePlayer = createTestPlayers(1, ["Solo"]);
|
|
356
|
+
const engine = createTestEngine(definition, singlePlayer);
|
|
357
|
+
|
|
358
|
+
const state = engine.getState();
|
|
359
|
+
expect(state.players).toHaveLength(1);
|
|
360
|
+
expect(state.players[0]?.name).toBe("Solo");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("should create engine with many players", () => {
|
|
364
|
+
const moves: GameMoveDefinitions<SimpleGameState, SimpleMoves> = {
|
|
365
|
+
incrementScore: { reducer: () => {} },
|
|
366
|
+
pass: { reducer: () => {} },
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const definition: GameDefinition<SimpleGameState, SimpleMoves> = {
|
|
370
|
+
name: "Test Game",
|
|
371
|
+
setup: (players) => ({
|
|
372
|
+
players: players.map((p) => ({
|
|
373
|
+
id: p.id,
|
|
374
|
+
name: p.name || "Player",
|
|
375
|
+
score: 0,
|
|
376
|
+
})),
|
|
377
|
+
currentPlayer: 0,
|
|
378
|
+
}),
|
|
379
|
+
moves,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const manyPlayers = createTestPlayers(6);
|
|
383
|
+
const engine = createTestEngine(definition, manyPlayers);
|
|
384
|
+
|
|
385
|
+
const state = engine.getState();
|
|
386
|
+
expect(state.players).toHaveLength(6);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RuleEngine, type RuleEngineOptions } from "../engine/rule-engine";
|
|
2
|
+
import type {
|
|
3
|
+
GameDefinition,
|
|
4
|
+
Player,
|
|
5
|
+
} from "../game-definition/game-definition";
|
|
6
|
+
import { createTestPlayers } from "./test-player-builder";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Test Engine Builder
|
|
10
|
+
*
|
|
11
|
+
* Task 2.3: Implement createTestEngine(definition, players?, options?)
|
|
12
|
+
*
|
|
13
|
+
* Creates a fully initialized RuleEngine for testing.
|
|
14
|
+
* Simplifies test setup by providing sensible defaults for players and options.
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Default 2 players if not provided
|
|
18
|
+
* - Optional seed for deterministic testing
|
|
19
|
+
* - Returns ready-to-use engine with initialized state
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // Create engine with defaults (2 players)
|
|
24
|
+
* const engine = createTestEngine(gameDefinition);
|
|
25
|
+
*
|
|
26
|
+
* // Create engine with custom players
|
|
27
|
+
* const players = createTestPlayers(4, ['Alice', 'Bob', 'Charlie', 'Dave']);
|
|
28
|
+
* const engine = createTestEngine(gameDefinition, players);
|
|
29
|
+
*
|
|
30
|
+
* // Create engine with seed for deterministic tests
|
|
31
|
+
* const engine = createTestEngine(gameDefinition, undefined, {
|
|
32
|
+
* seed: 'test-seed-123'
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function createTestEngine<TState, TMoves extends Record<string, any>>(
|
|
37
|
+
definition: GameDefinition<TState, TMoves>,
|
|
38
|
+
players?: Player[],
|
|
39
|
+
options?: RuleEngineOptions,
|
|
40
|
+
): RuleEngine<TState, TMoves> {
|
|
41
|
+
// Use provided players or create default 2 players
|
|
42
|
+
const enginePlayers = players ?? createTestPlayers(2);
|
|
43
|
+
|
|
44
|
+
// Create and return engine with options
|
|
45
|
+
return new RuleEngine(definition, enginePlayers, options);
|
|
46
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { RuleEngine } from "../engine/rule-engine";
|
|
3
|
+
import type { FlowDefinition } from "../flow/flow-definition";
|
|
4
|
+
import type { GameDefinition } from "../game-definition/game-definition";
|
|
5
|
+
import type { GameMoveDefinitions } from "../game-definition/move-definitions";
|
|
6
|
+
import { createPlayerId, type PlayerId } from "../types";
|
|
7
|
+
import { expectPhaseTransition } from "./test-flow-assertions";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Test state for flow assertions
|
|
11
|
+
*/
|
|
12
|
+
type FlowTestState = {
|
|
13
|
+
players: Array<{
|
|
14
|
+
id: PlayerId;
|
|
15
|
+
name: string;
|
|
16
|
+
}>;
|
|
17
|
+
phase: "draw" | "main" | "end";
|
|
18
|
+
turnNumber: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type FlowTestMoves = {
|
|
22
|
+
nextPhase: Record<string, never>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("test-flow-assertions", () => {
|
|
26
|
+
function createTestEngine() {
|
|
27
|
+
const moves: GameMoveDefinitions<FlowTestState, FlowTestMoves> = {
|
|
28
|
+
nextPhase: {
|
|
29
|
+
reducer: (draft) => {
|
|
30
|
+
// Transition through phases
|
|
31
|
+
if (draft.phase === "draw") {
|
|
32
|
+
draft.phase = "main";
|
|
33
|
+
} else if (draft.phase === "main") {
|
|
34
|
+
draft.phase = "end";
|
|
35
|
+
} else if (draft.phase === "end") {
|
|
36
|
+
draft.phase = "draw";
|
|
37
|
+
draft.turnNumber += 1;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const flow: FlowDefinition<FlowTestState> = {
|
|
44
|
+
turn: {
|
|
45
|
+
onBegin: (context) => {
|
|
46
|
+
context.state.phase = "draw";
|
|
47
|
+
},
|
|
48
|
+
phases: {
|
|
49
|
+
draw: {
|
|
50
|
+
order: 0,
|
|
51
|
+
next: "main",
|
|
52
|
+
},
|
|
53
|
+
main: {
|
|
54
|
+
order: 1,
|
|
55
|
+
next: "end",
|
|
56
|
+
},
|
|
57
|
+
end: {
|
|
58
|
+
order: 2,
|
|
59
|
+
next: undefined,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const gameDefinition: GameDefinition<FlowTestState, FlowTestMoves> = {
|
|
66
|
+
name: "Flow Test Game",
|
|
67
|
+
setup: (players) => ({
|
|
68
|
+
players: players.map((p) => ({
|
|
69
|
+
id: p.id as PlayerId,
|
|
70
|
+
name: p.name || "Player",
|
|
71
|
+
})),
|
|
72
|
+
phase: "draw" as const,
|
|
73
|
+
turnNumber: 1,
|
|
74
|
+
}),
|
|
75
|
+
moves,
|
|
76
|
+
flow,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const players = [
|
|
80
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
81
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
return new RuleEngine(gameDefinition, players);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("expectPhaseTransition", () => {
|
|
88
|
+
it("should pass when phase transitions correctly", () => {
|
|
89
|
+
const engine = createTestEngine();
|
|
90
|
+
|
|
91
|
+
// Should not throw
|
|
92
|
+
expectPhaseTransition(
|
|
93
|
+
engine,
|
|
94
|
+
"nextPhase",
|
|
95
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
96
|
+
"draw",
|
|
97
|
+
"main",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should throw when initial phase does not match", () => {
|
|
102
|
+
const engine = createTestEngine();
|
|
103
|
+
|
|
104
|
+
expect(() => {
|
|
105
|
+
expectPhaseTransition(
|
|
106
|
+
engine,
|
|
107
|
+
"nextPhase",
|
|
108
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
109
|
+
"main", // Wrong initial phase
|
|
110
|
+
"end",
|
|
111
|
+
);
|
|
112
|
+
}).toThrow(/Expected initial phase to be 'main'/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should throw when final phase does not match", () => {
|
|
116
|
+
const engine = createTestEngine();
|
|
117
|
+
|
|
118
|
+
expect(() => {
|
|
119
|
+
expectPhaseTransition(
|
|
120
|
+
engine,
|
|
121
|
+
"nextPhase",
|
|
122
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
123
|
+
"draw",
|
|
124
|
+
"end", // Wrong final phase (should be 'main')
|
|
125
|
+
);
|
|
126
|
+
}).toThrow(/Expected final phase to be 'end'/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should work with multiple transitions", () => {
|
|
130
|
+
const engine = createTestEngine();
|
|
131
|
+
|
|
132
|
+
expectPhaseTransition(
|
|
133
|
+
engine,
|
|
134
|
+
"nextPhase",
|
|
135
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
136
|
+
"draw",
|
|
137
|
+
"main",
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expectPhaseTransition(
|
|
141
|
+
engine,
|
|
142
|
+
"nextPhase",
|
|
143
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
144
|
+
"main",
|
|
145
|
+
"end",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expectPhaseTransition(
|
|
149
|
+
engine,
|
|
150
|
+
"nextPhase",
|
|
151
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
152
|
+
"end",
|
|
153
|
+
"draw",
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should handle move failures", () => {
|
|
158
|
+
const _engine = createTestEngine();
|
|
159
|
+
|
|
160
|
+
// Create a move that will fail
|
|
161
|
+
const moves: GameMoveDefinitions<FlowTestState, FlowTestMoves> = {
|
|
162
|
+
nextPhase: {
|
|
163
|
+
condition: () => false, // Always fails
|
|
164
|
+
reducer: () => {},
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const flow: FlowDefinition<FlowTestState> = {
|
|
169
|
+
turn: {
|
|
170
|
+
phases: {
|
|
171
|
+
draw: { order: 0, next: "main" },
|
|
172
|
+
main: { order: 1, next: undefined },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const gameDefinition: GameDefinition<FlowTestState, FlowTestMoves> = {
|
|
178
|
+
name: "Failing Flow Test",
|
|
179
|
+
setup: (players) => ({
|
|
180
|
+
players: players.map((p) => ({
|
|
181
|
+
id: p.id as PlayerId,
|
|
182
|
+
name: p.name || "Player",
|
|
183
|
+
})),
|
|
184
|
+
phase: "draw" as const,
|
|
185
|
+
turnNumber: 1,
|
|
186
|
+
}),
|
|
187
|
+
moves,
|
|
188
|
+
flow,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const failEngine = new RuleEngine(gameDefinition, [
|
|
192
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
expect(() => {
|
|
196
|
+
expectPhaseTransition(
|
|
197
|
+
failEngine,
|
|
198
|
+
"nextPhase",
|
|
199
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
200
|
+
"draw",
|
|
201
|
+
"main",
|
|
202
|
+
);
|
|
203
|
+
}).toThrow(/Move failed/);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should work with FlowManager integration", () => {
|
|
207
|
+
const engine = createTestEngine();
|
|
208
|
+
const flowManager = engine.getFlowManager();
|
|
209
|
+
|
|
210
|
+
expect(flowManager).toBeDefined();
|
|
211
|
+
expect(flowManager?.getCurrentPhase()).toBe("draw");
|
|
212
|
+
|
|
213
|
+
// Transition phase
|
|
214
|
+
expectPhaseTransition(
|
|
215
|
+
engine,
|
|
216
|
+
"nextPhase",
|
|
217
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
218
|
+
"draw",
|
|
219
|
+
"main",
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Note: FlowManager tracks its own phase state separate from game state
|
|
223
|
+
// This test verifies that expectPhaseTransition works with engines that have FlowManager
|
|
224
|
+
const state = engine.getState();
|
|
225
|
+
expect(state.phase).toBe("main");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should accept phase path for nested state", () => {
|
|
229
|
+
// Create engine with nested phase in state
|
|
230
|
+
type NestedFlowState = {
|
|
231
|
+
players: Array<{ id: PlayerId; name: string }>;
|
|
232
|
+
gameState: {
|
|
233
|
+
currentPhase: "start" | "middle" | "end";
|
|
234
|
+
};
|
|
235
|
+
turnNumber: number;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
type NestedMoves = {
|
|
239
|
+
advance: Record<string, never>;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const moves: GameMoveDefinitions<NestedFlowState, NestedMoves> = {
|
|
243
|
+
advance: {
|
|
244
|
+
reducer: (draft) => {
|
|
245
|
+
if (draft.gameState.currentPhase === "start") {
|
|
246
|
+
draft.gameState.currentPhase = "middle";
|
|
247
|
+
} else if (draft.gameState.currentPhase === "middle") {
|
|
248
|
+
draft.gameState.currentPhase = "end";
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const gameDefinition: GameDefinition<NestedFlowState, NestedMoves> = {
|
|
255
|
+
name: "Nested Flow Test",
|
|
256
|
+
setup: (players) => ({
|
|
257
|
+
players: players.map((p) => ({
|
|
258
|
+
id: p.id as PlayerId,
|
|
259
|
+
name: p.name || "Player",
|
|
260
|
+
})),
|
|
261
|
+
gameState: {
|
|
262
|
+
currentPhase: "start" as const,
|
|
263
|
+
},
|
|
264
|
+
turnNumber: 1,
|
|
265
|
+
}),
|
|
266
|
+
moves,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const nestedEngine = new RuleEngine(gameDefinition, [
|
|
270
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
// Should work with custom phase path
|
|
274
|
+
expectPhaseTransition(
|
|
275
|
+
nestedEngine,
|
|
276
|
+
"advance",
|
|
277
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
278
|
+
"start",
|
|
279
|
+
"middle",
|
|
280
|
+
"gameState.currentPhase",
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|