@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,756 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { FlowDefinition } from "../flow-definition";
|
|
3
|
+
import { FlowManager } from "../flow-manager";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Task 9.3, 9.4: Tests for FlowManager
|
|
7
|
+
*
|
|
8
|
+
* Tests verify:
|
|
9
|
+
* - Turn/phase/step state machine construction
|
|
10
|
+
* - Lifecycle hook execution
|
|
11
|
+
* - Automatic and programmatic transitions
|
|
12
|
+
* - Hierarchical state management
|
|
13
|
+
* - Event handling
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type GameState = {
|
|
17
|
+
currentPlayer: number;
|
|
18
|
+
players: Array<{ id: string; ready: boolean }>;
|
|
19
|
+
turnCount: number;
|
|
20
|
+
phase?: string;
|
|
21
|
+
step?: string;
|
|
22
|
+
log: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("FlowManager - State Machine", () => {
|
|
26
|
+
describe("Task 9.3, 9.4: Turn/Phase/Step State Machine", () => {
|
|
27
|
+
it("should initialize with turn → phase hierarchy", () => {
|
|
28
|
+
// Red: Test initialization
|
|
29
|
+
const flow: FlowDefinition<GameState> = {
|
|
30
|
+
gameSegments: {
|
|
31
|
+
mainGame: {
|
|
32
|
+
order: 1,
|
|
33
|
+
turn: {
|
|
34
|
+
phases: {
|
|
35
|
+
ready: { order: 0, next: "draw" },
|
|
36
|
+
draw: { order: 1, next: "main" },
|
|
37
|
+
main: { order: 2, next: undefined },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const initialState: GameState = {
|
|
45
|
+
currentPlayer: 0,
|
|
46
|
+
players: [{ id: "p1", ready: false }],
|
|
47
|
+
turnCount: 0,
|
|
48
|
+
log: [],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const manager = new FlowManager(flow, initialState);
|
|
52
|
+
|
|
53
|
+
const state = manager.getState();
|
|
54
|
+
expect(state).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should progress through phases sequentially", () => {
|
|
58
|
+
const flow: FlowDefinition<GameState> = {
|
|
59
|
+
gameSegments: {
|
|
60
|
+
mainGame: {
|
|
61
|
+
order: 1,
|
|
62
|
+
turn: {
|
|
63
|
+
phases: {
|
|
64
|
+
ready: { order: 0, next: "draw" },
|
|
65
|
+
draw: { order: 1, next: "main" },
|
|
66
|
+
main: { order: 2, next: undefined },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const initialState: GameState = {
|
|
74
|
+
currentPlayer: 0,
|
|
75
|
+
players: [{ id: "p1", ready: false }],
|
|
76
|
+
turnCount: 0,
|
|
77
|
+
log: [],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const manager = new FlowManager(flow, initialState);
|
|
81
|
+
|
|
82
|
+
// Should start in ready phase
|
|
83
|
+
expect(manager.getCurrentPhase()).toBe("ready");
|
|
84
|
+
|
|
85
|
+
// Transition to draw
|
|
86
|
+
manager.nextPhase();
|
|
87
|
+
expect(manager.getCurrentPhase()).toBe("draw");
|
|
88
|
+
|
|
89
|
+
// Transition to main
|
|
90
|
+
manager.nextPhase();
|
|
91
|
+
expect(manager.getCurrentPhase()).toBe("main");
|
|
92
|
+
|
|
93
|
+
// Next phase is undefined, should end turn
|
|
94
|
+
manager.nextPhase();
|
|
95
|
+
expect(manager.getCurrentPhase()).toBe("ready"); // New turn
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should support hierarchical states (phase → steps)", () => {
|
|
99
|
+
// Task 9.13: Hierarchical states
|
|
100
|
+
const flow: FlowDefinition<GameState> = {
|
|
101
|
+
gameSegments: {
|
|
102
|
+
mainGame: {
|
|
103
|
+
order: 1,
|
|
104
|
+
turn: {
|
|
105
|
+
phases: {
|
|
106
|
+
main: {
|
|
107
|
+
order: 0,
|
|
108
|
+
next: undefined,
|
|
109
|
+
steps: {
|
|
110
|
+
declare: { order: 0, next: "target" },
|
|
111
|
+
target: { order: 1, next: "damage" },
|
|
112
|
+
damage: { order: 2, next: undefined },
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const initialState: GameState = {
|
|
122
|
+
currentPlayer: 0,
|
|
123
|
+
players: [{ id: "p1", ready: false }],
|
|
124
|
+
turnCount: 0,
|
|
125
|
+
log: [],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const manager = new FlowManager(flow, initialState);
|
|
129
|
+
|
|
130
|
+
expect(manager.getCurrentPhase()).toBe("main");
|
|
131
|
+
expect(manager.getCurrentStep()).toBe("declare");
|
|
132
|
+
|
|
133
|
+
manager.nextStep();
|
|
134
|
+
expect(manager.getCurrentStep()).toBe("target");
|
|
135
|
+
|
|
136
|
+
manager.nextStep();
|
|
137
|
+
expect(manager.getCurrentStep()).toBe("damage");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("Task 9.5, 9.6: Lifecycle Hooks", () => {
|
|
142
|
+
it("should execute onBegin hook when phase starts", () => {
|
|
143
|
+
const flow: FlowDefinition<GameState> = {
|
|
144
|
+
gameSegments: {
|
|
145
|
+
mainGame: {
|
|
146
|
+
order: 1,
|
|
147
|
+
turn: {
|
|
148
|
+
onBegin: (context) => {
|
|
149
|
+
context.state.turnCount += 1;
|
|
150
|
+
context.state.log.push("turn-begin");
|
|
151
|
+
},
|
|
152
|
+
phases: {
|
|
153
|
+
ready: {
|
|
154
|
+
order: 0,
|
|
155
|
+
next: undefined,
|
|
156
|
+
onBegin: (context) => {
|
|
157
|
+
context.state.log.push("ready-begin");
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const initialState: GameState = {
|
|
167
|
+
currentPlayer: 0,
|
|
168
|
+
players: [{ id: "p1", ready: false }],
|
|
169
|
+
turnCount: 0,
|
|
170
|
+
log: [],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const manager = new FlowManager(flow, initialState);
|
|
174
|
+
const state = manager.getGameState();
|
|
175
|
+
|
|
176
|
+
expect(state.turnCount).toBe(1);
|
|
177
|
+
expect(state.log).toContain("turn-begin");
|
|
178
|
+
expect(state.log).toContain("ready-begin");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should execute onEnd hook when phase ends", () => {
|
|
182
|
+
const flow: FlowDefinition<GameState> = {
|
|
183
|
+
gameSegments: {
|
|
184
|
+
mainGame: {
|
|
185
|
+
order: 1,
|
|
186
|
+
turn: {
|
|
187
|
+
phases: {
|
|
188
|
+
ready: {
|
|
189
|
+
order: 0,
|
|
190
|
+
next: "draw",
|
|
191
|
+
onEnd: (context) => {
|
|
192
|
+
context.state.log.push("ready-end");
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
draw: {
|
|
196
|
+
order: 1,
|
|
197
|
+
next: undefined,
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const initialState: GameState = {
|
|
206
|
+
currentPlayer: 0,
|
|
207
|
+
players: [{ id: "p1", ready: false }],
|
|
208
|
+
turnCount: 0,
|
|
209
|
+
log: [],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const manager = new FlowManager(flow, initialState);
|
|
213
|
+
|
|
214
|
+
manager.nextPhase();
|
|
215
|
+
const state = manager.getGameState();
|
|
216
|
+
|
|
217
|
+
expect(state.log).toContain("ready-end");
|
|
218
|
+
expect(manager.getCurrentPhase()).toBe("draw");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should execute step lifecycle hooks", () => {
|
|
222
|
+
const flow: FlowDefinition<GameState> = {
|
|
223
|
+
gameSegments: {
|
|
224
|
+
mainGame: {
|
|
225
|
+
order: 1,
|
|
226
|
+
turn: {
|
|
227
|
+
phases: {
|
|
228
|
+
main: {
|
|
229
|
+
order: 0,
|
|
230
|
+
next: undefined,
|
|
231
|
+
steps: {
|
|
232
|
+
declare: {
|
|
233
|
+
order: 0,
|
|
234
|
+
next: "target",
|
|
235
|
+
onBegin: (context) => {
|
|
236
|
+
context.state.log.push("declare-begin");
|
|
237
|
+
},
|
|
238
|
+
onEnd: (context) => {
|
|
239
|
+
context.state.log.push("declare-end");
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
target: {
|
|
243
|
+
order: 1,
|
|
244
|
+
next: undefined,
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const initialState: GameState = {
|
|
255
|
+
currentPlayer: 0,
|
|
256
|
+
players: [{ id: "p1", ready: false }],
|
|
257
|
+
turnCount: 0,
|
|
258
|
+
log: [],
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const manager = new FlowManager(flow, initialState);
|
|
262
|
+
let state = manager.getGameState();
|
|
263
|
+
|
|
264
|
+
expect(state.log).toContain("declare-begin");
|
|
265
|
+
|
|
266
|
+
manager.nextStep();
|
|
267
|
+
state = manager.getGameState();
|
|
268
|
+
|
|
269
|
+
expect(state.log).toContain("declare-end");
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("Task 9.7, 9.8: EndIf Conditions", () => {
|
|
274
|
+
it("should automatically transition when endIf returns true", () => {
|
|
275
|
+
const flow: FlowDefinition<GameState> = {
|
|
276
|
+
gameSegments: {
|
|
277
|
+
mainGame: {
|
|
278
|
+
order: 1,
|
|
279
|
+
turn: {
|
|
280
|
+
phases: {
|
|
281
|
+
ready: {
|
|
282
|
+
order: 0,
|
|
283
|
+
next: "draw",
|
|
284
|
+
endIf: (context) => {
|
|
285
|
+
// Auto-end when all players are ready
|
|
286
|
+
return context.state.players.every((p) => p.ready);
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
draw: {
|
|
290
|
+
order: 1,
|
|
291
|
+
next: undefined,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const initialState: GameState = {
|
|
300
|
+
currentPlayer: 0,
|
|
301
|
+
players: [
|
|
302
|
+
{ id: "p1", ready: false },
|
|
303
|
+
{ id: "p2", ready: false },
|
|
304
|
+
],
|
|
305
|
+
turnCount: 0,
|
|
306
|
+
log: [],
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const manager = new FlowManager(flow, initialState);
|
|
310
|
+
|
|
311
|
+
expect(manager.getCurrentPhase()).toBe("ready");
|
|
312
|
+
|
|
313
|
+
// Make all players ready
|
|
314
|
+
manager.updateState((draft) => {
|
|
315
|
+
for (const player of draft.players) {
|
|
316
|
+
player.ready = true;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Should auto-transition to draw
|
|
321
|
+
expect(manager.getCurrentPhase()).toBe("draw");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should check endIf after state updates", () => {
|
|
325
|
+
const flow: FlowDefinition<GameState> = {
|
|
326
|
+
gameSegments: {
|
|
327
|
+
mainGame: {
|
|
328
|
+
order: 1,
|
|
329
|
+
turn: {
|
|
330
|
+
phases: {
|
|
331
|
+
main: {
|
|
332
|
+
order: 0,
|
|
333
|
+
next: "end",
|
|
334
|
+
endIf: (context) => context.state.turnCount >= 5,
|
|
335
|
+
},
|
|
336
|
+
end: {
|
|
337
|
+
order: 1,
|
|
338
|
+
next: undefined,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const initialState: GameState = {
|
|
347
|
+
currentPlayer: 0,
|
|
348
|
+
players: [{ id: "p1", ready: false }],
|
|
349
|
+
turnCount: 0,
|
|
350
|
+
log: [],
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const manager = new FlowManager(flow, initialState);
|
|
354
|
+
|
|
355
|
+
expect(manager.getCurrentPhase()).toBe("main");
|
|
356
|
+
|
|
357
|
+
// Increment turn count
|
|
358
|
+
manager.updateState((draft) => {
|
|
359
|
+
draft.turnCount = 5;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Should auto-transition to end
|
|
363
|
+
expect(manager.getCurrentPhase()).toBe("end");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe("Task 9.9, 9.10: FlowContext", () => {
|
|
368
|
+
it("should provide programmatic endPhase control", () => {
|
|
369
|
+
const flow: FlowDefinition<GameState> = {
|
|
370
|
+
gameSegments: {
|
|
371
|
+
mainGame: {
|
|
372
|
+
order: 1,
|
|
373
|
+
turn: {
|
|
374
|
+
phases: {
|
|
375
|
+
ready: {
|
|
376
|
+
order: 0,
|
|
377
|
+
next: "draw",
|
|
378
|
+
onBegin: (context) => {
|
|
379
|
+
// Skip this phase if no players
|
|
380
|
+
if (context.state.players.length === 0) {
|
|
381
|
+
context.endPhase();
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
draw: {
|
|
386
|
+
order: 1,
|
|
387
|
+
next: undefined,
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const initialState: GameState = {
|
|
396
|
+
currentPlayer: 0,
|
|
397
|
+
players: [],
|
|
398
|
+
turnCount: 0,
|
|
399
|
+
log: [],
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const manager = new FlowManager(flow, initialState);
|
|
403
|
+
|
|
404
|
+
// Should have skipped ready phase via endPhase()
|
|
405
|
+
expect(manager.getCurrentPhase()).toBe("draw");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should provide programmatic endStep control", () => {
|
|
409
|
+
const flow: FlowDefinition<GameState> = {
|
|
410
|
+
gameSegments: {
|
|
411
|
+
mainGame: {
|
|
412
|
+
order: 1,
|
|
413
|
+
turn: {
|
|
414
|
+
phases: {
|
|
415
|
+
main: {
|
|
416
|
+
order: 0,
|
|
417
|
+
next: undefined,
|
|
418
|
+
steps: {
|
|
419
|
+
declare: {
|
|
420
|
+
order: 0,
|
|
421
|
+
next: "target",
|
|
422
|
+
onBegin: (context) => {
|
|
423
|
+
if (context.state.players.length === 0) {
|
|
424
|
+
context.endStep();
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
target: {
|
|
429
|
+
order: 1,
|
|
430
|
+
next: undefined,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const initialState: GameState = {
|
|
441
|
+
currentPlayer: 0,
|
|
442
|
+
players: [],
|
|
443
|
+
turnCount: 0,
|
|
444
|
+
log: [],
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const manager = new FlowManager(flow, initialState);
|
|
448
|
+
|
|
449
|
+
// Should have skipped declare step
|
|
450
|
+
expect(manager.getCurrentStep()).toBe("target");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("should provide programmatic endTurn control", () => {
|
|
454
|
+
const flow: FlowDefinition<GameState> = {
|
|
455
|
+
gameSegments: {
|
|
456
|
+
mainGame: {
|
|
457
|
+
order: 1,
|
|
458
|
+
turn: {
|
|
459
|
+
onBegin: (context) => {
|
|
460
|
+
context.state.turnCount += 1;
|
|
461
|
+
// End turn immediately if turn count is 5
|
|
462
|
+
if (context.state.turnCount === 5) {
|
|
463
|
+
context.endTurn();
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
phases: {
|
|
467
|
+
main: { order: 0, next: undefined },
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const initialState: GameState = {
|
|
475
|
+
currentPlayer: 0,
|
|
476
|
+
players: [{ id: "p1", ready: false }],
|
|
477
|
+
turnCount: 4, // Next turn will be 5
|
|
478
|
+
log: [],
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const manager = new FlowManager(flow, initialState);
|
|
482
|
+
|
|
483
|
+
manager.nextTurn();
|
|
484
|
+
const state = manager.getGameState();
|
|
485
|
+
|
|
486
|
+
// Turn should have ended immediately
|
|
487
|
+
expect(state.turnCount).toBeGreaterThan(5);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("should provide current flow information", () => {
|
|
491
|
+
const flow: FlowDefinition<GameState> = {
|
|
492
|
+
gameSegments: {
|
|
493
|
+
mainGame: {
|
|
494
|
+
order: 1,
|
|
495
|
+
turn: {
|
|
496
|
+
phases: {
|
|
497
|
+
main: {
|
|
498
|
+
order: 0,
|
|
499
|
+
next: undefined,
|
|
500
|
+
onBegin: (context) => {
|
|
501
|
+
// For games that don't have special setup, we should set currentPlayer
|
|
502
|
+
// in the turn onBegin. For testing, we'll set it here.
|
|
503
|
+
if (!context.getCurrentPlayer()) {
|
|
504
|
+
context.setCurrentPlayer("p1");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Access flow information
|
|
508
|
+
const phase = context.getCurrentPhase();
|
|
509
|
+
const turn = context.getTurnNumber();
|
|
510
|
+
const player = context.getCurrentPlayer();
|
|
511
|
+
|
|
512
|
+
expect(phase).toBe("main");
|
|
513
|
+
expect(turn).toBeGreaterThan(0);
|
|
514
|
+
expect(player).toBeDefined();
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const initialState: GameState = {
|
|
524
|
+
currentPlayer: 0,
|
|
525
|
+
players: [{ id: "p1", ready: false }],
|
|
526
|
+
turnCount: 0,
|
|
527
|
+
log: [],
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const manager = new FlowManager(flow, initialState);
|
|
531
|
+
manager.getGameState(); // Trigger onBegin
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
describe("Task 9.11, 9.12: Flow Event Handling", () => {
|
|
536
|
+
it("should handle NEXT_PHASE event", () => {
|
|
537
|
+
const flow: FlowDefinition<GameState> = {
|
|
538
|
+
gameSegments: {
|
|
539
|
+
mainGame: {
|
|
540
|
+
order: 1,
|
|
541
|
+
turn: {
|
|
542
|
+
phases: {
|
|
543
|
+
ready: { order: 0, next: "draw" },
|
|
544
|
+
draw: { order: 1, next: undefined },
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const initialState: GameState = {
|
|
552
|
+
currentPlayer: 0,
|
|
553
|
+
players: [{ id: "p1", ready: false }],
|
|
554
|
+
turnCount: 0,
|
|
555
|
+
log: [],
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const manager = new FlowManager(flow, initialState);
|
|
559
|
+
|
|
560
|
+
expect(manager.getCurrentPhase()).toBe("ready");
|
|
561
|
+
|
|
562
|
+
manager.send({ type: "NEXT_PHASE" });
|
|
563
|
+
|
|
564
|
+
expect(manager.getCurrentPhase()).toBe("draw");
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("should handle END_TURN event", () => {
|
|
568
|
+
const flow: FlowDefinition<GameState> = {
|
|
569
|
+
gameSegments: {
|
|
570
|
+
mainGame: {
|
|
571
|
+
order: 1,
|
|
572
|
+
turn: {
|
|
573
|
+
onBegin: (context) => {
|
|
574
|
+
context.state.turnCount += 1;
|
|
575
|
+
},
|
|
576
|
+
phases: {
|
|
577
|
+
main: { order: 0, next: undefined },
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const initialState: GameState = {
|
|
585
|
+
currentPlayer: 0,
|
|
586
|
+
players: [{ id: "p1", ready: false }],
|
|
587
|
+
turnCount: 0,
|
|
588
|
+
log: [],
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const manager = new FlowManager(flow, initialState);
|
|
592
|
+
|
|
593
|
+
const initialTurnCount = manager.getGameState().turnCount;
|
|
594
|
+
|
|
595
|
+
manager.send({ type: "END_TURN" });
|
|
596
|
+
|
|
597
|
+
expect(manager.getGameState().turnCount).toBeGreaterThan(
|
|
598
|
+
initialTurnCount,
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("should handle END_STEP event", () => {
|
|
603
|
+
const flow: FlowDefinition<GameState> = {
|
|
604
|
+
gameSegments: {
|
|
605
|
+
mainGame: {
|
|
606
|
+
order: 1,
|
|
607
|
+
turn: {
|
|
608
|
+
phases: {
|
|
609
|
+
main: {
|
|
610
|
+
order: 0,
|
|
611
|
+
next: undefined,
|
|
612
|
+
steps: {
|
|
613
|
+
declare: { order: 0, next: "target" },
|
|
614
|
+
target: { order: 1, next: undefined },
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
const initialState: GameState = {
|
|
624
|
+
currentPlayer: 0,
|
|
625
|
+
players: [{ id: "p1", ready: false }],
|
|
626
|
+
turnCount: 0,
|
|
627
|
+
log: [],
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const manager = new FlowManager(flow, initialState);
|
|
631
|
+
|
|
632
|
+
expect(manager.getCurrentStep()).toBe("declare");
|
|
633
|
+
|
|
634
|
+
manager.send({ type: "END_STEP" });
|
|
635
|
+
|
|
636
|
+
expect(manager.getCurrentStep()).toBe("target");
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
describe("Task 9.13, 9.14: Hierarchical States", () => {
|
|
641
|
+
it("should support nested phase → step hierarchy", () => {
|
|
642
|
+
const flow: FlowDefinition<GameState> = {
|
|
643
|
+
gameSegments: {
|
|
644
|
+
mainGame: {
|
|
645
|
+
order: 1,
|
|
646
|
+
turn: {
|
|
647
|
+
phases: {
|
|
648
|
+
main: {
|
|
649
|
+
order: 0,
|
|
650
|
+
next: "end",
|
|
651
|
+
steps: {
|
|
652
|
+
start: { order: 0, next: "middle" },
|
|
653
|
+
middle: { order: 1, next: "finish" },
|
|
654
|
+
finish: { order: 2, next: undefined },
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
end: {
|
|
658
|
+
order: 1,
|
|
659
|
+
next: undefined,
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const initialState: GameState = {
|
|
668
|
+
currentPlayer: 0,
|
|
669
|
+
players: [{ id: "p1", ready: false }],
|
|
670
|
+
turnCount: 0,
|
|
671
|
+
log: [],
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const manager = new FlowManager(flow, initialState);
|
|
675
|
+
|
|
676
|
+
// Should start in main.start
|
|
677
|
+
expect(manager.getCurrentPhase()).toBe("main");
|
|
678
|
+
expect(manager.getCurrentStep()).toBe("start");
|
|
679
|
+
|
|
680
|
+
// Progress through steps
|
|
681
|
+
manager.nextStep();
|
|
682
|
+
expect(manager.getCurrentStep()).toBe("middle");
|
|
683
|
+
|
|
684
|
+
manager.nextStep();
|
|
685
|
+
expect(manager.getCurrentStep()).toBe("finish");
|
|
686
|
+
|
|
687
|
+
// End step should end phase
|
|
688
|
+
manager.nextStep();
|
|
689
|
+
expect(manager.getCurrentPhase()).toBe("end");
|
|
690
|
+
expect(manager.getCurrentStep()).toBeUndefined();
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it("should handle lifecycle hooks at all levels", () => {
|
|
694
|
+
const flow: FlowDefinition<GameState> = {
|
|
695
|
+
gameSegments: {
|
|
696
|
+
mainGame: {
|
|
697
|
+
order: 1,
|
|
698
|
+
turn: {
|
|
699
|
+
onBegin: (context) => {
|
|
700
|
+
context.state.log.push("turn-begin");
|
|
701
|
+
},
|
|
702
|
+
onEnd: (context) => {
|
|
703
|
+
context.state.log.push("turn-end");
|
|
704
|
+
},
|
|
705
|
+
phases: {
|
|
706
|
+
main: {
|
|
707
|
+
order: 0,
|
|
708
|
+
next: undefined,
|
|
709
|
+
onBegin: (context) => {
|
|
710
|
+
context.state.log.push("phase-begin");
|
|
711
|
+
},
|
|
712
|
+
onEnd: (context) => {
|
|
713
|
+
context.state.log.push("phase-end");
|
|
714
|
+
},
|
|
715
|
+
steps: {
|
|
716
|
+
start: {
|
|
717
|
+
order: 0,
|
|
718
|
+
next: undefined,
|
|
719
|
+
onBegin: (context) => {
|
|
720
|
+
context.state.log.push("step-begin");
|
|
721
|
+
},
|
|
722
|
+
onEnd: (context) => {
|
|
723
|
+
context.state.log.push("step-end");
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const initialState: GameState = {
|
|
735
|
+
currentPlayer: 0,
|
|
736
|
+
players: [{ id: "p1", ready: false }],
|
|
737
|
+
turnCount: 0,
|
|
738
|
+
log: [],
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
const manager = new FlowManager(flow, initialState);
|
|
742
|
+
let state = manager.getGameState();
|
|
743
|
+
|
|
744
|
+
expect(state.log).toContain("turn-begin");
|
|
745
|
+
expect(state.log).toContain("phase-begin");
|
|
746
|
+
expect(state.log).toContain("step-begin");
|
|
747
|
+
|
|
748
|
+
manager.nextTurn();
|
|
749
|
+
state = manager.getGameState();
|
|
750
|
+
|
|
751
|
+
expect(state.log).toContain("step-end");
|
|
752
|
+
expect(state.log).toContain("phase-end");
|
|
753
|
+
expect(state.log).toContain("turn-end");
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
});
|