@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,641 @@
|
|
|
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
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Task 15: Example Game Implementation - Coin Flip Game
|
|
10
|
+
*
|
|
11
|
+
* Simple game to validate the entire @drmxrcy/tcg-core framework:
|
|
12
|
+
* - Players take turns flipping a coin (using seeded RNG)
|
|
13
|
+
* - Heads = score +1, Tails = no score
|
|
14
|
+
* - First player to reach 3 points wins
|
|
15
|
+
*
|
|
16
|
+
* Tests verify:
|
|
17
|
+
* - Game setup and initialization
|
|
18
|
+
* - Turn-based gameplay with flow management
|
|
19
|
+
* - Move execution with RNG integration
|
|
20
|
+
* - Win condition checking
|
|
21
|
+
* - Complete game playthrough
|
|
22
|
+
* - Deterministic replay
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type CoinFlipGameState = {
|
|
26
|
+
players: Array<{
|
|
27
|
+
id: PlayerId;
|
|
28
|
+
name: string;
|
|
29
|
+
score: number;
|
|
30
|
+
}>;
|
|
31
|
+
currentPlayerIndex: number;
|
|
32
|
+
turnNumber: number;
|
|
33
|
+
phase: "flip" | "ended";
|
|
34
|
+
lastFlipResult?: "heads" | "tails";
|
|
35
|
+
winner?: PlayerId;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type CoinFlipMoves = {
|
|
39
|
+
flipCoin: Record<string, never>;
|
|
40
|
+
endTurn: Record<string, never>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("Coin Flip Game - Setup", () => {
|
|
44
|
+
describe("Task 15.1, 15.2: Game Definition and Setup", () => {
|
|
45
|
+
it("should create game definition for coin flip", () => {
|
|
46
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
47
|
+
flipCoin: {
|
|
48
|
+
condition: (state) => state.phase === "flip",
|
|
49
|
+
reducer: (draft, _context) => {
|
|
50
|
+
// Access RNG through context (we'll pass it from engine)
|
|
51
|
+
const isHeads = Math.random() >= 0.5; // Placeholder
|
|
52
|
+
draft.lastFlipResult = isHeads ? "heads" : "tails";
|
|
53
|
+
|
|
54
|
+
if (isHeads) {
|
|
55
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
56
|
+
if (player) {
|
|
57
|
+
player.score += 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
endTurn: {
|
|
63
|
+
condition: (state) => state.phase === "flip",
|
|
64
|
+
reducer: (draft) => {
|
|
65
|
+
// Next player
|
|
66
|
+
draft.currentPlayerIndex =
|
|
67
|
+
(draft.currentPlayerIndex + 1) % draft.players.length;
|
|
68
|
+
draft.turnNumber += 1;
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
74
|
+
name: "Coin Flip",
|
|
75
|
+
setup: (players) => ({
|
|
76
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
77
|
+
id: p.id as PlayerId,
|
|
78
|
+
name: p.name || "Player",
|
|
79
|
+
score: 0,
|
|
80
|
+
})),
|
|
81
|
+
currentPlayerIndex: 0,
|
|
82
|
+
turnNumber: 1,
|
|
83
|
+
phase: "flip" as const,
|
|
84
|
+
}),
|
|
85
|
+
moves,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
expect(gameDefinition.name).toBe("Coin Flip");
|
|
89
|
+
expect(gameDefinition.setup).toBeFunction();
|
|
90
|
+
expect(gameDefinition.moves.flipCoin).toBeDefined();
|
|
91
|
+
expect(gameDefinition.moves.endTurn).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should initialize game state correctly", () => {
|
|
95
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
96
|
+
flipCoin: { reducer: () => {} },
|
|
97
|
+
endTurn: { reducer: () => {} },
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
101
|
+
name: "Coin Flip",
|
|
102
|
+
setup: (players) => ({
|
|
103
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
104
|
+
id: p.id as PlayerId,
|
|
105
|
+
name: p.name || "Player",
|
|
106
|
+
score: 0,
|
|
107
|
+
})),
|
|
108
|
+
currentPlayerIndex: 0,
|
|
109
|
+
turnNumber: 1,
|
|
110
|
+
phase: "flip" as const,
|
|
111
|
+
}),
|
|
112
|
+
moves,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const players = [
|
|
116
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
117
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
121
|
+
const state = engine.getState();
|
|
122
|
+
|
|
123
|
+
expect(state.players).toHaveLength(2);
|
|
124
|
+
expect(state.players[0]?.name).toBe("Alice");
|
|
125
|
+
expect(state.players[1]?.name).toBe("Bob");
|
|
126
|
+
expect(state.currentPlayerIndex).toBe(0);
|
|
127
|
+
expect(state.turnNumber).toBe(1);
|
|
128
|
+
expect(state.phase).toBe("flip");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("Task 15.3, 15.4: Game Moves", () => {
|
|
133
|
+
it("should implement flipCoin move with RNG", () => {
|
|
134
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
135
|
+
flipCoin: {
|
|
136
|
+
reducer: (draft) => {
|
|
137
|
+
// In real implementation, would use engine.getRNG()
|
|
138
|
+
const isHeads = Math.random() >= 0.5;
|
|
139
|
+
draft.lastFlipResult = isHeads ? "heads" : "tails";
|
|
140
|
+
|
|
141
|
+
if (isHeads) {
|
|
142
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
143
|
+
if (player) {
|
|
144
|
+
player.score += 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
endTurn: { reducer: () => {} },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
153
|
+
name: "Coin Flip",
|
|
154
|
+
setup: (players) => ({
|
|
155
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
156
|
+
id: p.id as PlayerId,
|
|
157
|
+
name: p.name || "Player",
|
|
158
|
+
score: 0,
|
|
159
|
+
})),
|
|
160
|
+
currentPlayerIndex: 0,
|
|
161
|
+
turnNumber: 1,
|
|
162
|
+
phase: "flip" as const,
|
|
163
|
+
}),
|
|
164
|
+
moves,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const players = [
|
|
168
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
169
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const engine = new RuleEngine(gameDefinition, players, {
|
|
173
|
+
seed: "test-seed",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = engine.executeMove("flipCoin", {
|
|
177
|
+
playerId: createPlayerId("p1"),
|
|
178
|
+
params: {},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(result.success).toBe(true);
|
|
182
|
+
|
|
183
|
+
const state = engine.getState();
|
|
184
|
+
expect(state.lastFlipResult).toBeDefined();
|
|
185
|
+
expect(["heads", "tails"]).toContain(state.lastFlipResult);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should implement endTurn move to progress to next player", () => {
|
|
189
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
190
|
+
flipCoin: { reducer: () => {} },
|
|
191
|
+
endTurn: {
|
|
192
|
+
reducer: (draft) => {
|
|
193
|
+
draft.currentPlayerIndex =
|
|
194
|
+
(draft.currentPlayerIndex + 1) % draft.players.length;
|
|
195
|
+
draft.turnNumber += 1;
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
201
|
+
name: "Coin Flip",
|
|
202
|
+
setup: (players) => ({
|
|
203
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
204
|
+
id: p.id as PlayerId,
|
|
205
|
+
name: p.name || "Player",
|
|
206
|
+
score: 0,
|
|
207
|
+
})),
|
|
208
|
+
currentPlayerIndex: 0,
|
|
209
|
+
turnNumber: 1,
|
|
210
|
+
phase: "flip" as const,
|
|
211
|
+
}),
|
|
212
|
+
moves,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const players = [
|
|
216
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
217
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
221
|
+
|
|
222
|
+
expect(engine.getState().currentPlayerIndex).toBe(0);
|
|
223
|
+
|
|
224
|
+
engine.executeMove("endTurn", {
|
|
225
|
+
playerId: createPlayerId("p1"),
|
|
226
|
+
params: {},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const state = engine.getState();
|
|
230
|
+
expect(state.currentPlayerIndex).toBe(1);
|
|
231
|
+
expect(state.turnNumber).toBe(2);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("Task 15.5, 15.6: Game Flow", () => {
|
|
236
|
+
it("should define turn-based flow", () => {
|
|
237
|
+
const flow: FlowDefinition<CoinFlipGameState> = {
|
|
238
|
+
turn: {
|
|
239
|
+
onBegin: (context) => {
|
|
240
|
+
context.state.phase = "flip";
|
|
241
|
+
},
|
|
242
|
+
phases: {
|
|
243
|
+
flip: {
|
|
244
|
+
order: 0,
|
|
245
|
+
next: undefined,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
expect(flow.turn).toBeDefined();
|
|
252
|
+
expect(flow.turn.phases?.flip).toBeDefined();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should integrate flow with game", () => {
|
|
256
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
257
|
+
flipCoin: { reducer: () => {} },
|
|
258
|
+
endTurn: { reducer: () => {} },
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const flow: FlowDefinition<CoinFlipGameState> = {
|
|
262
|
+
turn: {
|
|
263
|
+
onBegin: (context) => {
|
|
264
|
+
context.state.phase = "flip";
|
|
265
|
+
},
|
|
266
|
+
phases: {
|
|
267
|
+
flip: { order: 0, next: undefined },
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
273
|
+
name: "Coin Flip",
|
|
274
|
+
setup: (players) => ({
|
|
275
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
276
|
+
id: p.id as PlayerId,
|
|
277
|
+
name: p.name || "Player",
|
|
278
|
+
score: 0,
|
|
279
|
+
})),
|
|
280
|
+
currentPlayerIndex: 0,
|
|
281
|
+
turnNumber: 1,
|
|
282
|
+
phase: "flip" as const,
|
|
283
|
+
}),
|
|
284
|
+
moves,
|
|
285
|
+
flow,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const players = [
|
|
289
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
290
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
294
|
+
const flowManager = engine.getFlowManager();
|
|
295
|
+
|
|
296
|
+
expect(flowManager).toBeDefined();
|
|
297
|
+
expect(flowManager?.getCurrentPhase()).toBe("flip");
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("Task 15.7, 15.8: End Conditions", () => {
|
|
302
|
+
it("should define win condition (first to 3 points)", () => {
|
|
303
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
304
|
+
flipCoin: { reducer: () => {} },
|
|
305
|
+
endTurn: { reducer: () => {} },
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
309
|
+
name: "Coin Flip",
|
|
310
|
+
setup: (players) => ({
|
|
311
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
312
|
+
id: p.id as PlayerId,
|
|
313
|
+
name: p.name || "Player",
|
|
314
|
+
score: 0,
|
|
315
|
+
})),
|
|
316
|
+
currentPlayerIndex: 0,
|
|
317
|
+
turnNumber: 1,
|
|
318
|
+
phase: "flip" as const,
|
|
319
|
+
}),
|
|
320
|
+
moves,
|
|
321
|
+
endIf: (state) => {
|
|
322
|
+
const winner = state.players.find((p) => p.score >= 3);
|
|
323
|
+
if (winner) {
|
|
324
|
+
return {
|
|
325
|
+
winner: winner.id,
|
|
326
|
+
reason: "Reached 3 points",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return undefined;
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const players = [
|
|
334
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
335
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
339
|
+
|
|
340
|
+
// Game should not be ended yet
|
|
341
|
+
expect(engine.checkGameEnd()).toBeUndefined();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("should detect game end when player reaches 3 points", () => {
|
|
345
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
346
|
+
flipCoin: {
|
|
347
|
+
reducer: (draft) => {
|
|
348
|
+
// Force heads for testing
|
|
349
|
+
draft.lastFlipResult = "heads";
|
|
350
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
351
|
+
if (player) {
|
|
352
|
+
player.score += 1;
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
endTurn: { reducer: () => {} },
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
360
|
+
name: "Coin Flip",
|
|
361
|
+
setup: (players) => ({
|
|
362
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
363
|
+
id: p.id as PlayerId,
|
|
364
|
+
name: p.name || "Player",
|
|
365
|
+
score: 0,
|
|
366
|
+
})),
|
|
367
|
+
currentPlayerIndex: 0,
|
|
368
|
+
turnNumber: 1,
|
|
369
|
+
phase: "flip" as const,
|
|
370
|
+
}),
|
|
371
|
+
moves,
|
|
372
|
+
endIf: (state) => {
|
|
373
|
+
const winner = state.players.find((p) => p.score >= 3);
|
|
374
|
+
if (winner) {
|
|
375
|
+
return {
|
|
376
|
+
winner: winner.id,
|
|
377
|
+
reason: "Reached 3 points",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return undefined;
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const players = [
|
|
385
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
386
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
390
|
+
|
|
391
|
+
// Flip 3 times to win
|
|
392
|
+
engine.executeMove("flipCoin", {
|
|
393
|
+
playerId: createPlayerId("p1"),
|
|
394
|
+
params: {},
|
|
395
|
+
});
|
|
396
|
+
engine.executeMove("flipCoin", {
|
|
397
|
+
playerId: createPlayerId("p1"),
|
|
398
|
+
params: {},
|
|
399
|
+
});
|
|
400
|
+
engine.executeMove("flipCoin", {
|
|
401
|
+
playerId: createPlayerId("p1"),
|
|
402
|
+
params: {},
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const gameEnd = engine.checkGameEnd();
|
|
406
|
+
expect(gameEnd).toBeDefined();
|
|
407
|
+
expect(gameEnd?.winner).toBe(createPlayerId("p1"));
|
|
408
|
+
expect(gameEnd?.reason).toBe("Reached 3 points");
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe("Task 15.9, 15.10: Complete Game Playthrough", () => {
|
|
413
|
+
it("should play complete game from start to finish", () => {
|
|
414
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
415
|
+
flipCoin: {
|
|
416
|
+
reducer: (draft) => {
|
|
417
|
+
// Use deterministic "random" for test
|
|
418
|
+
const isHeads = Math.random() >= 0.5;
|
|
419
|
+
draft.lastFlipResult = isHeads ? "heads" : "tails";
|
|
420
|
+
|
|
421
|
+
if (isHeads) {
|
|
422
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
423
|
+
if (player) {
|
|
424
|
+
player.score += 1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
endTurn: {
|
|
430
|
+
reducer: (draft) => {
|
|
431
|
+
draft.currentPlayerIndex =
|
|
432
|
+
(draft.currentPlayerIndex + 1) % draft.players.length;
|
|
433
|
+
draft.turnNumber += 1;
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
439
|
+
name: "Coin Flip",
|
|
440
|
+
setup: (players) => ({
|
|
441
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
442
|
+
id: p.id as PlayerId,
|
|
443
|
+
name: p.name || "Player",
|
|
444
|
+
score: 0,
|
|
445
|
+
})),
|
|
446
|
+
currentPlayerIndex: 0,
|
|
447
|
+
turnNumber: 1,
|
|
448
|
+
phase: "flip" as const,
|
|
449
|
+
}),
|
|
450
|
+
moves,
|
|
451
|
+
endIf: (state) => {
|
|
452
|
+
const winner = state.players.find((p) => p.score >= 3);
|
|
453
|
+
if (winner) {
|
|
454
|
+
return {
|
|
455
|
+
winner: winner.id,
|
|
456
|
+
reason: "Reached 3 points",
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
return undefined;
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const players = [
|
|
464
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
465
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
466
|
+
];
|
|
467
|
+
|
|
468
|
+
const engine = new RuleEngine(gameDefinition, players, {
|
|
469
|
+
seed: "playthrough-test",
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
let gameEnd = engine.checkGameEnd();
|
|
473
|
+
let turn = 0;
|
|
474
|
+
const maxTurns = 50; // Safety limit
|
|
475
|
+
|
|
476
|
+
// Play until someone wins or max turns reached
|
|
477
|
+
while (!gameEnd && turn < maxTurns) {
|
|
478
|
+
const currentPlayer =
|
|
479
|
+
engine.getState().players[engine.getState().currentPlayerIndex];
|
|
480
|
+
|
|
481
|
+
// Flip coin
|
|
482
|
+
if (currentPlayer?.id) {
|
|
483
|
+
engine.executeMove("flipCoin", {
|
|
484
|
+
playerId: currentPlayer.id,
|
|
485
|
+
params: {},
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// End turn
|
|
489
|
+
engine.executeMove("endTurn", {
|
|
490
|
+
playerId: currentPlayer.id,
|
|
491
|
+
params: {},
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
gameEnd = engine.checkGameEnd();
|
|
496
|
+
turn++;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Game should have ended
|
|
500
|
+
expect(gameEnd).toBeDefined();
|
|
501
|
+
expect(turn).toBeLessThan(maxTurns);
|
|
502
|
+
|
|
503
|
+
// Winner should have at least 3 points
|
|
504
|
+
const finalState = engine.getState();
|
|
505
|
+
const winningPlayer = finalState.players.find(
|
|
506
|
+
(p) => p.id === gameEnd?.winner,
|
|
507
|
+
);
|
|
508
|
+
expect(winningPlayer?.score).toBeGreaterThanOrEqual(3);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe("Task 15.11, 15.12: Deterministic Replay", () => {
|
|
513
|
+
it("should support undo/redo for game history", () => {
|
|
514
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
515
|
+
flipCoin: {
|
|
516
|
+
reducer: (draft) => {
|
|
517
|
+
// Force heads for deterministic test
|
|
518
|
+
draft.lastFlipResult = "heads";
|
|
519
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
520
|
+
if (player) {
|
|
521
|
+
player.score += 1;
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
endTurn: {
|
|
526
|
+
reducer: (draft) => {
|
|
527
|
+
draft.currentPlayerIndex =
|
|
528
|
+
(draft.currentPlayerIndex + 1) % draft.players.length;
|
|
529
|
+
draft.turnNumber += 1;
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
535
|
+
name: "Coin Flip",
|
|
536
|
+
setup: (players) => ({
|
|
537
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
538
|
+
id: p.id as PlayerId,
|
|
539
|
+
name: p.name || "Player",
|
|
540
|
+
score: 0,
|
|
541
|
+
})),
|
|
542
|
+
currentPlayerIndex: 0,
|
|
543
|
+
turnNumber: 1,
|
|
544
|
+
phase: "flip" as const,
|
|
545
|
+
}),
|
|
546
|
+
moves,
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const players = [
|
|
550
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
551
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
552
|
+
];
|
|
553
|
+
|
|
554
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
555
|
+
|
|
556
|
+
// Execute some moves
|
|
557
|
+
engine.executeMove("flipCoin", {
|
|
558
|
+
playerId: createPlayerId("p1"),
|
|
559
|
+
params: {},
|
|
560
|
+
});
|
|
561
|
+
engine.executeMove("endTurn", {
|
|
562
|
+
playerId: createPlayerId("p1"),
|
|
563
|
+
params: {},
|
|
564
|
+
});
|
|
565
|
+
engine.executeMove("flipCoin", {
|
|
566
|
+
playerId: createPlayerId("p2"),
|
|
567
|
+
params: {},
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
expect(engine.getState().players[0]?.score).toBe(1);
|
|
571
|
+
expect(engine.getState().players[1]?.score).toBe(1);
|
|
572
|
+
|
|
573
|
+
// Undo last move
|
|
574
|
+
engine.undo();
|
|
575
|
+
expect(engine.getState().players[1]?.score).toBe(0);
|
|
576
|
+
|
|
577
|
+
// Redo
|
|
578
|
+
engine.redo();
|
|
579
|
+
expect(engine.getState().players[1]?.score).toBe(1);
|
|
580
|
+
|
|
581
|
+
// History tracking
|
|
582
|
+
const history = engine.getHistory();
|
|
583
|
+
expect(history.length).toBe(3);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("should track patches for network synchronization", () => {
|
|
587
|
+
const moves: GameMoveDefinitions<CoinFlipGameState, CoinFlipMoves> = {
|
|
588
|
+
flipCoin: {
|
|
589
|
+
reducer: (draft) => {
|
|
590
|
+
draft.lastFlipResult = "heads";
|
|
591
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
592
|
+
if (player) {
|
|
593
|
+
player.score += 1;
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
endTurn: { reducer: () => {} },
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const gameDefinition: GameDefinition<CoinFlipGameState, CoinFlipMoves> = {
|
|
601
|
+
name: "Coin Flip",
|
|
602
|
+
setup: (players) => ({
|
|
603
|
+
players: players.map((p): CoinFlipGameState["players"][number] => ({
|
|
604
|
+
id: p.id as PlayerId,
|
|
605
|
+
name: p.name || "Player",
|
|
606
|
+
score: 0,
|
|
607
|
+
})),
|
|
608
|
+
currentPlayerIndex: 0,
|
|
609
|
+
turnNumber: 1,
|
|
610
|
+
phase: "flip" as const,
|
|
611
|
+
}),
|
|
612
|
+
moves,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const players = [
|
|
616
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
617
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
618
|
+
];
|
|
619
|
+
|
|
620
|
+
const engine = new RuleEngine(gameDefinition, players);
|
|
621
|
+
|
|
622
|
+
// Execute move and capture patches
|
|
623
|
+
const result = engine.executeMove("flipCoin", {
|
|
624
|
+
playerId: createPlayerId("p1"),
|
|
625
|
+
params: {},
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
expect(result.success).toBe(true);
|
|
629
|
+
if (result.success) {
|
|
630
|
+
// Patches should be captured for network sync
|
|
631
|
+
expect(result.patches).toBeDefined();
|
|
632
|
+
expect(result.patches.length).toBeGreaterThan(0);
|
|
633
|
+
expect(result.inversePatches).toBeDefined();
|
|
634
|
+
|
|
635
|
+
// Get accumulated patches
|
|
636
|
+
const allPatches = engine.getPatches();
|
|
637
|
+
expect(allPatches.length).toBeGreaterThan(0);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
});
|