@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,161 @@
|
|
|
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 {
|
|
7
|
+
createTestCards,
|
|
8
|
+
createTestDeck,
|
|
9
|
+
createTestHand,
|
|
10
|
+
expectDeterministicReplay,
|
|
11
|
+
expectGameEnd,
|
|
12
|
+
expectGameNotEnded,
|
|
13
|
+
expectMoveFailure,
|
|
14
|
+
expectMoveSuccess,
|
|
15
|
+
expectPhaseTransition,
|
|
16
|
+
expectStateProperty,
|
|
17
|
+
withSeed,
|
|
18
|
+
} from "../index";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Integration test demonstrating @drmxrcy/tcg-core/testing utilities
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type TestGameState = {
|
|
25
|
+
players: Array<{
|
|
26
|
+
id: PlayerId;
|
|
27
|
+
name: string;
|
|
28
|
+
health: number;
|
|
29
|
+
hand: string[];
|
|
30
|
+
}>;
|
|
31
|
+
phase: "draw" | "main";
|
|
32
|
+
winner?: PlayerId;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type TestGameMoves = {
|
|
36
|
+
drawCard: Record<string, never>;
|
|
37
|
+
attack: Record<string, never>;
|
|
38
|
+
endPhase: Record<string, never>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("Testing Utilities Integration", () => {
|
|
42
|
+
it("demonstrates testing utilities workflow", () => {
|
|
43
|
+
// 1. Test factories
|
|
44
|
+
const cards = createTestCards(5);
|
|
45
|
+
expect(cards.length).toBe(5);
|
|
46
|
+
|
|
47
|
+
const deck = createTestDeck([], createPlayerId("p1"));
|
|
48
|
+
expect(deck.config.visibility).toBe("secret");
|
|
49
|
+
|
|
50
|
+
const hand = createTestHand([], createPlayerId("p1"));
|
|
51
|
+
expect(hand.config.visibility).toBe("private");
|
|
52
|
+
|
|
53
|
+
// 2. Test RNG
|
|
54
|
+
const shuffled1 = withSeed("test", (rng) => rng.shuffle([1, 2, 3]));
|
|
55
|
+
const shuffled2 = withSeed("test", (rng) => rng.shuffle([1, 2, 3]));
|
|
56
|
+
expect(shuffled1).toEqual(shuffled2);
|
|
57
|
+
|
|
58
|
+
// 3. Test game
|
|
59
|
+
const moves: GameMoveDefinitions<TestGameState, TestGameMoves> = {
|
|
60
|
+
drawCard: {
|
|
61
|
+
condition: (state) => state.phase === "draw",
|
|
62
|
+
reducer: (draft) => {
|
|
63
|
+
const player = draft.players[0];
|
|
64
|
+
if (player) {
|
|
65
|
+
player.hand.push("card");
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
attack: {
|
|
70
|
+
condition: (state) => state.phase === "main",
|
|
71
|
+
reducer: (draft) => {
|
|
72
|
+
const target = draft.players[1];
|
|
73
|
+
if (target) {
|
|
74
|
+
target.health -= 1;
|
|
75
|
+
if (target.health <= 0) {
|
|
76
|
+
draft.winner = draft.players[0]?.id;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
endPhase: {
|
|
82
|
+
reducer: (draft) => {
|
|
83
|
+
draft.phase = draft.phase === "draw" ? "main" : "draw";
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const gameDefinition: GameDefinition<TestGameState, TestGameMoves> = {
|
|
89
|
+
name: "Test Game",
|
|
90
|
+
setup: (players) => ({
|
|
91
|
+
players: players.map((p) => ({
|
|
92
|
+
id: p.id as PlayerId,
|
|
93
|
+
name: p.name || "Player",
|
|
94
|
+
health: 3,
|
|
95
|
+
hand: [],
|
|
96
|
+
})),
|
|
97
|
+
phase: "draw" as const,
|
|
98
|
+
}),
|
|
99
|
+
moves,
|
|
100
|
+
endIf: (state) => {
|
|
101
|
+
if (state.winner) {
|
|
102
|
+
return { winner: state.winner, reason: "Victory" };
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const engine = new RuleEngine(
|
|
109
|
+
gameDefinition,
|
|
110
|
+
[
|
|
111
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
112
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
113
|
+
],
|
|
114
|
+
{ seed: "test-seed" },
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// 4. Test assertions
|
|
118
|
+
expectStateProperty(engine, "phase", "draw");
|
|
119
|
+
expectStateProperty(engine, "players[0].health", 3);
|
|
120
|
+
|
|
121
|
+
expectMoveSuccess(engine, "drawCard", {
|
|
122
|
+
playerId: createPlayerId("p1"),
|
|
123
|
+
params: {},
|
|
124
|
+
});
|
|
125
|
+
expectStateProperty(engine, "players[0].hand.length", 1);
|
|
126
|
+
|
|
127
|
+
expectPhaseTransition(
|
|
128
|
+
engine,
|
|
129
|
+
"endPhase",
|
|
130
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
131
|
+
"draw",
|
|
132
|
+
"main",
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expectMoveFailure(
|
|
136
|
+
engine,
|
|
137
|
+
"drawCard",
|
|
138
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
139
|
+
"CONDITION_FAILED",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expectGameNotEnded(engine);
|
|
143
|
+
|
|
144
|
+
// Attack to end game
|
|
145
|
+
expectMoveSuccess(engine, "attack", {
|
|
146
|
+
playerId: createPlayerId("p1"),
|
|
147
|
+
params: {},
|
|
148
|
+
});
|
|
149
|
+
expectMoveSuccess(engine, "attack", {
|
|
150
|
+
playerId: createPlayerId("p1"),
|
|
151
|
+
params: {},
|
|
152
|
+
});
|
|
153
|
+
expectMoveSuccess(engine, "attack", {
|
|
154
|
+
playerId: createPlayerId("p1"),
|
|
155
|
+
params: {},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expectGameEnd(engine, createPlayerId("p1"));
|
|
159
|
+
expectDeterministicReplay(engine);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @drmxrcy/tcg-core/testing
|
|
3
|
+
*
|
|
4
|
+
* Testing utilities for game engine development
|
|
5
|
+
*
|
|
6
|
+
* Provides assertions, factories, and helpers for:
|
|
7
|
+
* - Move execution testing
|
|
8
|
+
* - State verification
|
|
9
|
+
* - Flow and phase transitions
|
|
10
|
+
* - Game end conditions
|
|
11
|
+
* - Card and zone creation
|
|
12
|
+
* - Deterministic RNG testing
|
|
13
|
+
* - Replay verification
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import {
|
|
18
|
+
* expectMoveSuccess,
|
|
19
|
+
* expectStateProperty,
|
|
20
|
+
* createTestCard,
|
|
21
|
+
* createTestDeck,
|
|
22
|
+
* withSeed,
|
|
23
|
+
* expectDeterministicReplay
|
|
24
|
+
* } from '@drmxrcy/tcg-core/testing';
|
|
25
|
+
*
|
|
26
|
+
* // Test move execution
|
|
27
|
+
* expectMoveSuccess(engine, 'playCard', {
|
|
28
|
+
* playerId: 'p1',
|
|
29
|
+
* data: { cardId: 'card-123' }
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Verify state
|
|
33
|
+
* expectStateProperty(engine, 'players[0].score', 10);
|
|
34
|
+
*
|
|
35
|
+
* // Create test data
|
|
36
|
+
* const card = createTestCard({ type: 'creature', basePower: 3 });
|
|
37
|
+
* const deck = createTestDeck(['card1', 'card2', 'card3'], 'player1');
|
|
38
|
+
*
|
|
39
|
+
* // Test with deterministic RNG
|
|
40
|
+
* const result = withSeed('test-seed', (rng) => {
|
|
41
|
+
* return rng.shuffle([1, 2, 3, 4, 5]);
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Verify deterministic replay
|
|
45
|
+
* expectDeterministicReplay(engine);
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @module @drmxrcy/tcg-core/testing
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
// Move assertions
|
|
52
|
+
export {
|
|
53
|
+
expectMoveFailure,
|
|
54
|
+
expectMoveSuccess,
|
|
55
|
+
expectStateProperty,
|
|
56
|
+
} from "./test-assertions";
|
|
57
|
+
// Card factory
|
|
58
|
+
export {
|
|
59
|
+
createTestCard,
|
|
60
|
+
createTestCards,
|
|
61
|
+
resetCardCounter,
|
|
62
|
+
} from "./test-card-factory";
|
|
63
|
+
|
|
64
|
+
// End assertions
|
|
65
|
+
export { expectGameEnd, expectGameNotEnded } from "./test-end-assertions";
|
|
66
|
+
// Flow assertions
|
|
67
|
+
export { expectPhaseTransition } from "./test-flow-assertions";
|
|
68
|
+
// Replay assertions
|
|
69
|
+
export { expectDeterministicReplay } from "./test-replay-assertions";
|
|
70
|
+
|
|
71
|
+
// RNG helpers
|
|
72
|
+
export {
|
|
73
|
+
createDeterministicRNG,
|
|
74
|
+
createMultipleRNGs,
|
|
75
|
+
createPredictableSequence,
|
|
76
|
+
expectDeterministicBehavior,
|
|
77
|
+
testWithMultipleSeeds,
|
|
78
|
+
withSeed,
|
|
79
|
+
} from "./test-rng-helpers";
|
|
80
|
+
// Zone factory
|
|
81
|
+
export {
|
|
82
|
+
createTestDeck,
|
|
83
|
+
createTestGraveyard,
|
|
84
|
+
createTestHand,
|
|
85
|
+
createTestPlayArea,
|
|
86
|
+
createTestZone,
|
|
87
|
+
resetZoneCounter,
|
|
88
|
+
} from "./test-zone-factory";
|
|
@@ -0,0 +1,341 @@
|
|
|
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 {
|
|
7
|
+
expectMoveFailure,
|
|
8
|
+
expectMoveSuccess,
|
|
9
|
+
expectStateProperty,
|
|
10
|
+
} from "./test-assertions";
|
|
11
|
+
import { createMockContext } from "./test-context-factory";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Test state for assertions
|
|
15
|
+
*/
|
|
16
|
+
type TestGameState = {
|
|
17
|
+
players: Array<{
|
|
18
|
+
id: PlayerId;
|
|
19
|
+
name: string;
|
|
20
|
+
score: number;
|
|
21
|
+
hand: string[];
|
|
22
|
+
}>;
|
|
23
|
+
currentPlayerIndex: number;
|
|
24
|
+
turnNumber: number;
|
|
25
|
+
phase: "main" | "end";
|
|
26
|
+
nested: {
|
|
27
|
+
deep: {
|
|
28
|
+
value: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type TestMoves = {
|
|
34
|
+
addScore: { amount: number };
|
|
35
|
+
drawCard: Record<string, never>;
|
|
36
|
+
invalidMove: Record<string, never>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("test-assertions", () => {
|
|
40
|
+
function createTestEngine() {
|
|
41
|
+
const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
|
|
42
|
+
addScore: {
|
|
43
|
+
reducer: (draft, context) => {
|
|
44
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
45
|
+
if (player && context.params?.amount) {
|
|
46
|
+
player.score += context.params.amount as number;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
drawCard: {
|
|
51
|
+
reducer: (draft) => {
|
|
52
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
53
|
+
if (player) {
|
|
54
|
+
player.hand.push("card");
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
invalidMove: {
|
|
59
|
+
condition: () => false, // Always fails
|
|
60
|
+
reducer: () => {},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
|
|
65
|
+
name: "Test Game",
|
|
66
|
+
setup: (players) => ({
|
|
67
|
+
players: players.map((p) => ({
|
|
68
|
+
id: p.id as PlayerId,
|
|
69
|
+
name: p.name || "Player",
|
|
70
|
+
score: 0,
|
|
71
|
+
hand: [] as string[],
|
|
72
|
+
})),
|
|
73
|
+
currentPlayerIndex: 0,
|
|
74
|
+
turnNumber: 1,
|
|
75
|
+
phase: "main" as const,
|
|
76
|
+
nested: {
|
|
77
|
+
deep: {
|
|
78
|
+
value: 42,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
moves,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const players = [
|
|
86
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
87
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
return new RuleEngine(gameDefinition, players);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("expectMoveSuccess", () => {
|
|
94
|
+
it("should pass when move succeeds", () => {
|
|
95
|
+
const engine = createTestEngine();
|
|
96
|
+
|
|
97
|
+
// Should not throw
|
|
98
|
+
expectMoveSuccess(engine, "addScore", {
|
|
99
|
+
playerId: createPlayerId("p1"),
|
|
100
|
+
params: { amount: 5 },
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should fail when move does not succeed", () => {
|
|
105
|
+
const engine = createTestEngine();
|
|
106
|
+
|
|
107
|
+
// Should throw because condition fails
|
|
108
|
+
expect(() => {
|
|
109
|
+
expectMoveSuccess(engine, "invalidMove", {
|
|
110
|
+
playerId: createPlayerId("p1"),
|
|
111
|
+
params: {},
|
|
112
|
+
});
|
|
113
|
+
}).toThrow();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should return move result for further assertions", () => {
|
|
117
|
+
const engine = createTestEngine();
|
|
118
|
+
|
|
119
|
+
const result = expectMoveSuccess(engine, "addScore", {
|
|
120
|
+
playerId: createPlayerId("p1"),
|
|
121
|
+
params: { amount: 5 },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.success).toBe(true);
|
|
125
|
+
if (result.success) {
|
|
126
|
+
expect(result.patches.length).toBeGreaterThan(0);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("should throw with descriptive error message on failure", () => {
|
|
131
|
+
const engine = createTestEngine();
|
|
132
|
+
|
|
133
|
+
expect(() => {
|
|
134
|
+
expectMoveSuccess(engine, "invalidMove", {
|
|
135
|
+
playerId: createPlayerId("p1"),
|
|
136
|
+
params: {},
|
|
137
|
+
});
|
|
138
|
+
}).toThrow(/Expected move 'invalidMove' to succeed/);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("expectMoveFailure", () => {
|
|
143
|
+
it("should pass when move fails", () => {
|
|
144
|
+
const engine = createTestEngine();
|
|
145
|
+
|
|
146
|
+
// Should not throw
|
|
147
|
+
expectMoveFailure(engine, "invalidMove", {
|
|
148
|
+
playerId: createPlayerId("p1"),
|
|
149
|
+
params: {},
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should fail when move succeeds", () => {
|
|
154
|
+
const engine = createTestEngine();
|
|
155
|
+
|
|
156
|
+
// Should throw because move succeeds
|
|
157
|
+
expect(() => {
|
|
158
|
+
expectMoveFailure(engine, "addScore", {
|
|
159
|
+
playerId: createPlayerId("p1"),
|
|
160
|
+
params: { amount: 5 },
|
|
161
|
+
});
|
|
162
|
+
}).toThrow();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should return failure result with error", () => {
|
|
166
|
+
const engine = createTestEngine();
|
|
167
|
+
|
|
168
|
+
const result = expectMoveFailure(engine, "invalidMove", {
|
|
169
|
+
playerId: createPlayerId("p1"),
|
|
170
|
+
params: {},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(result.success).toBe(false);
|
|
174
|
+
if (!result.success) {
|
|
175
|
+
expect(result.error).toBeDefined();
|
|
176
|
+
expect(result.errorCode).toBeDefined();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should optionally check error code", () => {
|
|
181
|
+
const engine = createTestEngine();
|
|
182
|
+
|
|
183
|
+
// Should not throw because error code matches
|
|
184
|
+
expectMoveFailure(
|
|
185
|
+
engine,
|
|
186
|
+
"invalidMove",
|
|
187
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
188
|
+
"CONDITION_FAILED",
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should throw when error code does not match", () => {
|
|
193
|
+
const engine = createTestEngine();
|
|
194
|
+
|
|
195
|
+
expect(() => {
|
|
196
|
+
expectMoveFailure(
|
|
197
|
+
engine,
|
|
198
|
+
"invalidMove",
|
|
199
|
+
{ playerId: createPlayerId("p1"), params: {} },
|
|
200
|
+
"WRONG_CODE",
|
|
201
|
+
);
|
|
202
|
+
}).toThrow(/Expected error code 'WRONG_CODE'/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should throw with descriptive error message when move succeeds", () => {
|
|
206
|
+
const engine = createTestEngine();
|
|
207
|
+
|
|
208
|
+
expect(() => {
|
|
209
|
+
expectMoveFailure(engine, "addScore", {
|
|
210
|
+
playerId: createPlayerId("p1"),
|
|
211
|
+
params: { amount: 5 },
|
|
212
|
+
});
|
|
213
|
+
}).toThrow(/Expected move 'addScore' to fail/);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("expectStateProperty", () => {
|
|
218
|
+
it("should verify top-level property", () => {
|
|
219
|
+
const engine = createTestEngine();
|
|
220
|
+
|
|
221
|
+
// Should not throw
|
|
222
|
+
expectStateProperty(engine, "turnNumber", 1);
|
|
223
|
+
expectStateProperty(engine, "phase", "main");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should verify nested property with dot notation", () => {
|
|
227
|
+
const engine = createTestEngine();
|
|
228
|
+
|
|
229
|
+
// Should not throw
|
|
230
|
+
expectStateProperty(engine, "nested.deep.value", 42);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should verify array element", () => {
|
|
234
|
+
const engine = createTestEngine();
|
|
235
|
+
|
|
236
|
+
expectStateProperty(engine, "players[0].name", "Alice");
|
|
237
|
+
expectStateProperty(engine, "players[1].name", "Bob");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("should verify array length", () => {
|
|
241
|
+
const engine = createTestEngine();
|
|
242
|
+
|
|
243
|
+
expectStateProperty(engine, "players.length", 2);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should throw when property value does not match", () => {
|
|
247
|
+
const engine = createTestEngine();
|
|
248
|
+
|
|
249
|
+
expect(() => {
|
|
250
|
+
expectStateProperty(engine, "turnNumber", 99);
|
|
251
|
+
}).toThrow(/Expected state.turnNumber to be 99/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should throw when property path does not exist", () => {
|
|
255
|
+
const engine = createTestEngine();
|
|
256
|
+
|
|
257
|
+
expect(() => {
|
|
258
|
+
expectStateProperty(
|
|
259
|
+
engine,
|
|
260
|
+
"nonexistent.path" as any,
|
|
261
|
+
"anything" as any,
|
|
262
|
+
);
|
|
263
|
+
}).toThrow(/Property path 'nonexistent.path' not found/);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should work with undefined values", () => {
|
|
267
|
+
const engine = createTestEngine();
|
|
268
|
+
engine.executeMove("addScore", {
|
|
269
|
+
playerId: createPlayerId("p1"),
|
|
270
|
+
params: { amount: 5 },
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Non-existent property should be undefined
|
|
274
|
+
expect(() => {
|
|
275
|
+
expectStateProperty(
|
|
276
|
+
engine,
|
|
277
|
+
"players[0].nonexistent" as any,
|
|
278
|
+
undefined as any,
|
|
279
|
+
);
|
|
280
|
+
}).toThrow();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should verify state changes after moves", () => {
|
|
284
|
+
const engine = createTestEngine();
|
|
285
|
+
|
|
286
|
+
expectStateProperty(engine, "players[0].score", 0);
|
|
287
|
+
|
|
288
|
+
engine.executeMove("addScore", {
|
|
289
|
+
playerId: createPlayerId("p1"),
|
|
290
|
+
params: { amount: 10 },
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expectStateProperty(engine, "players[0].score", 10);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("should handle complex nested paths", () => {
|
|
297
|
+
const engine = createTestEngine();
|
|
298
|
+
|
|
299
|
+
// Draw some cards
|
|
300
|
+
engine.executeMove("drawCard", {
|
|
301
|
+
playerId: createPlayerId("p1"),
|
|
302
|
+
params: {},
|
|
303
|
+
});
|
|
304
|
+
engine.executeMove("drawCard", {
|
|
305
|
+
playerId: createPlayerId("p1"),
|
|
306
|
+
params: {},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
expectStateProperty(engine, "players[0].hand.length", 2);
|
|
310
|
+
expectStateProperty(engine, "players[0].hand[0]", "card");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("integration with RuleEngine", () => {
|
|
315
|
+
it("should work together in a typical test scenario", () => {
|
|
316
|
+
const engine = createTestEngine();
|
|
317
|
+
|
|
318
|
+
// Initial state assertions
|
|
319
|
+
expectStateProperty(engine, "turnNumber", 1);
|
|
320
|
+
expectStateProperty(engine, "players[0].score", 0);
|
|
321
|
+
|
|
322
|
+
// Execute and verify success
|
|
323
|
+
expectMoveSuccess(engine, "addScore", {
|
|
324
|
+
playerId: createPlayerId("p1"),
|
|
325
|
+
params: { amount: 5 },
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Verify state changed
|
|
329
|
+
expectStateProperty(engine, "players[0].score", 5);
|
|
330
|
+
|
|
331
|
+
// Verify invalid move fails
|
|
332
|
+
expectMoveFailure(engine, "invalidMove", {
|
|
333
|
+
playerId: createPlayerId("p1"),
|
|
334
|
+
params: {},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// State should be unchanged after failed move
|
|
338
|
+
expectStateProperty(engine, "players[0].score", 5);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|