@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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Context Factory
|
|
3
|
+
*
|
|
4
|
+
* Utilities for creating MoveContext objects in tests.
|
|
5
|
+
* Provides mock implementations of engine services for unit testing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { HistoryOperations } from "../history/history-operations";
|
|
9
|
+
import type { MoveContext, MoveContextInput } from "../moves/move-system";
|
|
10
|
+
import type { CardOperations } from "../operations/card-operations";
|
|
11
|
+
import type { CounterOperations } from "../operations/counter-operations";
|
|
12
|
+
import type { GameOperations } from "../operations/game-operations";
|
|
13
|
+
import type { ZoneOperations } from "../operations/zone-operations";
|
|
14
|
+
import { SeededRNG } from "../rng/seeded-rng";
|
|
15
|
+
import type { CardId, PlayerId, ZoneId } from "../types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a mock MoveContext for testing
|
|
19
|
+
*
|
|
20
|
+
* Builds a full MoveContext with mock implementations of engine services.
|
|
21
|
+
* Useful for unit testing reducers and conditions without a full engine.
|
|
22
|
+
*
|
|
23
|
+
* @param input - Partial context input
|
|
24
|
+
* @param options - Optional mock implementations
|
|
25
|
+
* @returns Full MoveContext with mocks
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const context = createMockContext({
|
|
30
|
+
* playerId: 'p1',
|
|
31
|
+
* params: { cardId: 'card-123' }
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Use in reducer test
|
|
35
|
+
* const reducer: MoveReducer<GameState> = (draft, ctx) => {
|
|
36
|
+
* // ctx has full MoveContext type
|
|
37
|
+
* };
|
|
38
|
+
* reducer(draft, context);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function createMockContext<TParams = any>(
|
|
42
|
+
input: MoveContextInput<TParams>,
|
|
43
|
+
options?: {
|
|
44
|
+
rng?: SeededRNG;
|
|
45
|
+
zones?: Partial<ZoneOperations>;
|
|
46
|
+
cards?: Partial<CardOperations<any>>;
|
|
47
|
+
game?: Partial<GameOperations>;
|
|
48
|
+
counters?: Partial<CounterOperations>;
|
|
49
|
+
registry?: any;
|
|
50
|
+
flow?: {
|
|
51
|
+
currentPhase?: string;
|
|
52
|
+
currentSegment?: string;
|
|
53
|
+
turn: number;
|
|
54
|
+
currentPlayer: PlayerId;
|
|
55
|
+
isFirstTurn: boolean;
|
|
56
|
+
endPhase?: () => void;
|
|
57
|
+
endSegment?: () => void;
|
|
58
|
+
endTurn?: () => void;
|
|
59
|
+
};
|
|
60
|
+
endGame?: (result: {
|
|
61
|
+
winner?: PlayerId;
|
|
62
|
+
reason: string;
|
|
63
|
+
metadata?: Record<string, unknown>;
|
|
64
|
+
}) => void;
|
|
65
|
+
trackers?: {
|
|
66
|
+
check(name: string, playerId?: PlayerId): boolean;
|
|
67
|
+
mark(name: string, playerId?: PlayerId): void;
|
|
68
|
+
unmark(name: string, playerId?: PlayerId): void;
|
|
69
|
+
};
|
|
70
|
+
history?: Partial<HistoryOperations>;
|
|
71
|
+
},
|
|
72
|
+
): MoveContext<TParams> {
|
|
73
|
+
const mockZones: ZoneOperations = {
|
|
74
|
+
moveCard: () => {},
|
|
75
|
+
getCardsInZone: () => [],
|
|
76
|
+
shuffleZone: () => {},
|
|
77
|
+
getCardZone: () => undefined,
|
|
78
|
+
drawCards: () => [],
|
|
79
|
+
mulligan: () => {},
|
|
80
|
+
bulkMove: () => [],
|
|
81
|
+
createDeck: () => [],
|
|
82
|
+
...options?.zones,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const mockCards: CardOperations<any> = {
|
|
86
|
+
getCardMeta: () => ({}),
|
|
87
|
+
updateCardMeta: () => {},
|
|
88
|
+
setCardMeta: () => {},
|
|
89
|
+
getCardOwner: () => undefined as any,
|
|
90
|
+
queryCards: () => [],
|
|
91
|
+
...options?.cards,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const mockGame: GameOperations = {
|
|
95
|
+
setOTP: () => {},
|
|
96
|
+
getOTP: () => undefined,
|
|
97
|
+
setChoosingFirstPlayer: () => {},
|
|
98
|
+
getChoosingFirstPlayer: () => undefined,
|
|
99
|
+
setPendingMulligan: () => {},
|
|
100
|
+
getPendingMulligan: () => [],
|
|
101
|
+
addPendingMulligan: () => {},
|
|
102
|
+
removePendingMulligan: () => {},
|
|
103
|
+
...options?.game,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const mockCounters: CounterOperations = {
|
|
107
|
+
setFlag: () => {},
|
|
108
|
+
getFlag: () => false,
|
|
109
|
+
addCounter: () => {},
|
|
110
|
+
removeCounter: () => {},
|
|
111
|
+
getCounter: () => 0,
|
|
112
|
+
clearCounter: () => {},
|
|
113
|
+
clearAllCounters: () => {},
|
|
114
|
+
getCardsWithFlag: () => [],
|
|
115
|
+
getCardsWithCounter: () => [],
|
|
116
|
+
...options?.counters,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
...input,
|
|
121
|
+
rng: options?.rng || new SeededRNG("test-seed"),
|
|
122
|
+
zones: mockZones,
|
|
123
|
+
cards: mockCards,
|
|
124
|
+
game: mockGame,
|
|
125
|
+
counters: mockCounters,
|
|
126
|
+
registry: options?.registry,
|
|
127
|
+
flow: options?.flow
|
|
128
|
+
? {
|
|
129
|
+
...options.flow,
|
|
130
|
+
endPhase: options.flow.endPhase || (() => {}),
|
|
131
|
+
endSegment: options.flow.endSegment || (() => {}),
|
|
132
|
+
endTurn: options.flow.endTurn || (() => {}),
|
|
133
|
+
}
|
|
134
|
+
: undefined,
|
|
135
|
+
endGame: options?.endGame || (() => {}),
|
|
136
|
+
trackers: options?.trackers || {
|
|
137
|
+
check: () => false,
|
|
138
|
+
mark: () => {},
|
|
139
|
+
unmark: () => {},
|
|
140
|
+
},
|
|
141
|
+
history: {
|
|
142
|
+
log: () => {},
|
|
143
|
+
...options?.history,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a mock CardOperations for testing
|
|
150
|
+
*
|
|
151
|
+
* @param overrides - Optional method overrides
|
|
152
|
+
* @returns Mock CardOperations
|
|
153
|
+
*/
|
|
154
|
+
export function createMockCardOperations<TCardMeta = any>(
|
|
155
|
+
overrides?: Partial<CardOperations<TCardMeta>>,
|
|
156
|
+
): CardOperations<TCardMeta> {
|
|
157
|
+
return {
|
|
158
|
+
getCardMeta: () => ({}) as TCardMeta,
|
|
159
|
+
updateCardMeta: () => {},
|
|
160
|
+
setCardMeta: () => {},
|
|
161
|
+
getCardOwner: () => undefined as any,
|
|
162
|
+
queryCards: () => [],
|
|
163
|
+
...overrides,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Create a mock ZoneOperations for testing
|
|
169
|
+
*
|
|
170
|
+
* @param overrides - Optional method overrides
|
|
171
|
+
* @returns Mock ZoneOperations
|
|
172
|
+
*/
|
|
173
|
+
export function createMockZoneOperations(
|
|
174
|
+
overrides?: Partial<ZoneOperations>,
|
|
175
|
+
): ZoneOperations {
|
|
176
|
+
return {
|
|
177
|
+
moveCard: () => {},
|
|
178
|
+
getCardsInZone: () => [],
|
|
179
|
+
shuffleZone: () => {},
|
|
180
|
+
getCardZone: () => undefined,
|
|
181
|
+
drawCards: () => [],
|
|
182
|
+
mulligan: () => {},
|
|
183
|
+
bulkMove: () => [],
|
|
184
|
+
createDeck: () => [],
|
|
185
|
+
...overrides,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { RuleEngine } from "../engine/rule-engine";
|
|
3
|
+
import type { GameDefinition } from "../game-definition/game-definition";
|
|
4
|
+
import type { GameMoveDefinitions } from "../game-definition/move-definitions";
|
|
5
|
+
import { createPlayerId, type PlayerId } from "../types";
|
|
6
|
+
import { expectGameEnd, expectGameNotEnded } from "./test-end-assertions";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Test state for end assertions
|
|
10
|
+
*/
|
|
11
|
+
type EndTestState = {
|
|
12
|
+
players: Array<{
|
|
13
|
+
id: PlayerId;
|
|
14
|
+
name: string;
|
|
15
|
+
health: number;
|
|
16
|
+
}>;
|
|
17
|
+
turnNumber: number;
|
|
18
|
+
winner?: PlayerId;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type EndTestMoves = {
|
|
22
|
+
damagePlayer: { targetId: PlayerId; amount: number };
|
|
23
|
+
setWinner: { winnerId: PlayerId };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("test-end-assertions", () => {
|
|
27
|
+
function createTestEngine() {
|
|
28
|
+
const moves: GameMoveDefinitions<EndTestState, EndTestMoves> = {
|
|
29
|
+
damagePlayer: {
|
|
30
|
+
reducer: (draft, context) => {
|
|
31
|
+
if (context.params?.targetId && context.params?.amount) {
|
|
32
|
+
const target = draft.players.find(
|
|
33
|
+
(p) => p.id === context.params?.targetId,
|
|
34
|
+
);
|
|
35
|
+
if (target) {
|
|
36
|
+
target.health -= context.params.amount as number;
|
|
37
|
+
if (target.health <= 0) {
|
|
38
|
+
// Set winner to the other player
|
|
39
|
+
const winner = draft.players.find(
|
|
40
|
+
(p) => p.id !== context.params?.targetId,
|
|
41
|
+
);
|
|
42
|
+
if (winner) {
|
|
43
|
+
draft.winner = winner.id;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
setWinner: {
|
|
51
|
+
reducer: (draft, context) => {
|
|
52
|
+
if (context.params?.winnerId) {
|
|
53
|
+
draft.winner = context.params.winnerId as PlayerId;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const gameDefinition: GameDefinition<EndTestState, EndTestMoves> = {
|
|
60
|
+
name: "End Test Game",
|
|
61
|
+
setup: (players) => ({
|
|
62
|
+
players: players.map((p) => ({
|
|
63
|
+
id: p.id as PlayerId,
|
|
64
|
+
name: p.name || "Player",
|
|
65
|
+
health: 10,
|
|
66
|
+
})),
|
|
67
|
+
turnNumber: 1,
|
|
68
|
+
}),
|
|
69
|
+
moves,
|
|
70
|
+
endIf: (state) => {
|
|
71
|
+
if (state.winner) {
|
|
72
|
+
return {
|
|
73
|
+
winner: state.winner,
|
|
74
|
+
reason: "Player eliminated",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const players = [
|
|
82
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
83
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
return new RuleEngine(gameDefinition, players);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("expectGameEnd", () => {
|
|
90
|
+
it("should pass when game ends with expected winner", () => {
|
|
91
|
+
const engine = createTestEngine();
|
|
92
|
+
|
|
93
|
+
// Set winner
|
|
94
|
+
engine.executeMove("setWinner", {
|
|
95
|
+
playerId: createPlayerId("p1"),
|
|
96
|
+
params: { winnerId: createPlayerId("p1") },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Should not throw
|
|
100
|
+
expectGameEnd(engine, createPlayerId("p1"));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should throw when game has not ended", () => {
|
|
104
|
+
const engine = createTestEngine();
|
|
105
|
+
|
|
106
|
+
expect(() => {
|
|
107
|
+
expectGameEnd(engine, createPlayerId("p1"));
|
|
108
|
+
}).toThrow(/Expected game to have ended/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should throw when winner does not match", () => {
|
|
112
|
+
const engine = createTestEngine();
|
|
113
|
+
|
|
114
|
+
engine.executeMove("setWinner", {
|
|
115
|
+
playerId: createPlayerId("p1"),
|
|
116
|
+
params: { winnerId: createPlayerId("p1") },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(() => {
|
|
120
|
+
expectGameEnd(engine, createPlayerId("p2"));
|
|
121
|
+
}).toThrow(/Expected winner to be 'p2'/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should work without specifying winner", () => {
|
|
125
|
+
const engine = createTestEngine();
|
|
126
|
+
|
|
127
|
+
engine.executeMove("setWinner", {
|
|
128
|
+
playerId: createPlayerId("p1"),
|
|
129
|
+
params: { winnerId: createPlayerId("p1") },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Should not throw - just checks that game ended
|
|
133
|
+
expectGameEnd(engine);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should optionally check reason", () => {
|
|
137
|
+
const engine = createTestEngine();
|
|
138
|
+
|
|
139
|
+
engine.executeMove("setWinner", {
|
|
140
|
+
playerId: createPlayerId("p1"),
|
|
141
|
+
params: { winnerId: createPlayerId("p1") },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Should not throw
|
|
145
|
+
expectGameEnd(engine, createPlayerId("p1"), "Player eliminated");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should throw when reason does not match", () => {
|
|
149
|
+
const engine = createTestEngine();
|
|
150
|
+
|
|
151
|
+
engine.executeMove("setWinner", {
|
|
152
|
+
playerId: createPlayerId("p1"),
|
|
153
|
+
params: { winnerId: createPlayerId("p1") },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(() => {
|
|
157
|
+
expectGameEnd(engine, createPlayerId("p1"), "Wrong reason");
|
|
158
|
+
}).toThrow(/Expected reason to be 'Wrong reason'/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should return game end result", () => {
|
|
162
|
+
const engine = createTestEngine();
|
|
163
|
+
|
|
164
|
+
engine.executeMove("setWinner", {
|
|
165
|
+
playerId: createPlayerId("p1"),
|
|
166
|
+
params: { winnerId: createPlayerId("p1") },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const result = expectGameEnd(engine);
|
|
170
|
+
expect(result.winner).toBe(createPlayerId("p1"));
|
|
171
|
+
expect(result.reason).toBe("Player eliminated");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should work in realistic game scenario", () => {
|
|
175
|
+
const engine = createTestEngine();
|
|
176
|
+
|
|
177
|
+
// Damage player 2 until they lose
|
|
178
|
+
engine.executeMove("damagePlayer", {
|
|
179
|
+
playerId: createPlayerId("p1"),
|
|
180
|
+
params: { targetId: createPlayerId("p2"), amount: 5 },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expectGameNotEnded(engine); // Game should still be ongoing
|
|
184
|
+
|
|
185
|
+
engine.executeMove("damagePlayer", {
|
|
186
|
+
playerId: createPlayerId("p1"),
|
|
187
|
+
params: { targetId: createPlayerId("p2"), amount: 5 },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Game should have ended
|
|
191
|
+
expectGameEnd(engine, createPlayerId("p1"), "Player eliminated");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("expectGameNotEnded", () => {
|
|
196
|
+
it("should pass when game has not ended", () => {
|
|
197
|
+
const engine = createTestEngine();
|
|
198
|
+
|
|
199
|
+
// Should not throw
|
|
200
|
+
expectGameNotEnded(engine);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should throw when game has ended", () => {
|
|
204
|
+
const engine = createTestEngine();
|
|
205
|
+
|
|
206
|
+
engine.executeMove("setWinner", {
|
|
207
|
+
playerId: createPlayerId("p1"),
|
|
208
|
+
params: { winnerId: createPlayerId("p1") },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(() => {
|
|
212
|
+
expectGameNotEnded(engine);
|
|
213
|
+
}).toThrow(/Expected game to still be ongoing/);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should provide helpful error message with end details", () => {
|
|
217
|
+
const engine = createTestEngine();
|
|
218
|
+
|
|
219
|
+
engine.executeMove("setWinner", {
|
|
220
|
+
playerId: createPlayerId("p1"),
|
|
221
|
+
params: { winnerId: createPlayerId("p1") },
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(() => {
|
|
225
|
+
expectGameNotEnded(engine);
|
|
226
|
+
}).toThrow(/"winner":"p1"/);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("integration", () => {
|
|
231
|
+
it("should work together to test game flow", () => {
|
|
232
|
+
const engine = createTestEngine();
|
|
233
|
+
|
|
234
|
+
// Initially game is not ended
|
|
235
|
+
expectGameNotEnded(engine);
|
|
236
|
+
|
|
237
|
+
// Do some damage but not enough to end game
|
|
238
|
+
engine.executeMove("damagePlayer", {
|
|
239
|
+
playerId: createPlayerId("p1"),
|
|
240
|
+
params: { targetId: createPlayerId("p2"), amount: 3 },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expectGameNotEnded(engine);
|
|
244
|
+
|
|
245
|
+
// More damage
|
|
246
|
+
engine.executeMove("damagePlayer", {
|
|
247
|
+
playerId: createPlayerId("p1"),
|
|
248
|
+
params: { targetId: createPlayerId("p2"), amount: 3 },
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expectGameNotEnded(engine);
|
|
252
|
+
|
|
253
|
+
// Final damage should end game
|
|
254
|
+
engine.executeMove("damagePlayer", {
|
|
255
|
+
playerId: createPlayerId("p1"),
|
|
256
|
+
params: { targetId: createPlayerId("p2"), amount: 4 },
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expectGameEnd(engine, createPlayerId("p1"));
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { RuleEngine } from "../engine/rule-engine";
|
|
2
|
+
import type { GameEndResult } from "../game-definition/game-definition";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Test End Assertions
|
|
6
|
+
*
|
|
7
|
+
* Assertion helpers for testing game end conditions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Assert that the game has ended
|
|
12
|
+
*
|
|
13
|
+
* Verifies that checkGameEnd returns a truthy value.
|
|
14
|
+
* Optionally checks the winner and/or reason.
|
|
15
|
+
*
|
|
16
|
+
* @param engine - Rule engine instance
|
|
17
|
+
* @param expectedWinner - Optional expected winner
|
|
18
|
+
* @param expectedReason - Optional expected reason
|
|
19
|
+
* @returns Game end result
|
|
20
|
+
* @throws Error if game hasn't ended or end result doesn't match expectations
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // Check that game ended
|
|
25
|
+
* expectGameEnd(engine);
|
|
26
|
+
*
|
|
27
|
+
* // Check specific winner
|
|
28
|
+
* expectGameEnd(engine, 'player1');
|
|
29
|
+
*
|
|
30
|
+
* // Check winner and reason
|
|
31
|
+
* expectGameEnd(engine, 'player1', 'Opponent eliminated');
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function expectGameEnd<TState, TMoves extends Record<string, any>>(
|
|
35
|
+
engine: RuleEngine<TState, TMoves>,
|
|
36
|
+
expectedWinner?: string,
|
|
37
|
+
expectedReason?: string,
|
|
38
|
+
): GameEndResult {
|
|
39
|
+
const endResult = engine.checkGameEnd();
|
|
40
|
+
|
|
41
|
+
if (!endResult) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Expected game to have ended, but checkGameEnd() returned undefined",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check winner if specified
|
|
48
|
+
if (expectedWinner !== undefined && endResult.winner !== expectedWinner) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Expected winner to be '${expectedWinner}', but got '${endResult.winner}'`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check reason if specified
|
|
55
|
+
if (expectedReason !== undefined && endResult.reason !== expectedReason) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Expected reason to be '${expectedReason}', but got '${endResult.reason}'`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return endResult;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Assert that the game has not ended
|
|
66
|
+
*
|
|
67
|
+
* Verifies that checkGameEnd returns undefined/falsy.
|
|
68
|
+
* Useful for testing that intermediate game states don't trigger end conditions.
|
|
69
|
+
*
|
|
70
|
+
* @param engine - Rule engine instance
|
|
71
|
+
* @throws Error if game has ended
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* // Verify game is still ongoing
|
|
76
|
+
* expectGameNotEnded(engine);
|
|
77
|
+
*
|
|
78
|
+
* // Do some moves
|
|
79
|
+
* engine.executeMove('attack', { ... });
|
|
80
|
+
*
|
|
81
|
+
* // Verify game still hasn't ended
|
|
82
|
+
* expectGameNotEnded(engine);
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function expectGameNotEnded<TState, TMoves extends Record<string, any>>(
|
|
86
|
+
engine: RuleEngine<TState, TMoves>,
|
|
87
|
+
): void {
|
|
88
|
+
const endResult = engine.checkGameEnd();
|
|
89
|
+
|
|
90
|
+
if (endResult) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Expected game to still be ongoing, but it ended with: ${JSON.stringify(endResult)}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|