@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,124 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createTestEngine } from "../testing/test-engine-builder";
|
|
3
|
+
import { createTestPlayers } from "../testing/test-player-builder";
|
|
4
|
+
import { createMockRiftboundGame } from "./createMockRiftboundGame";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Riftbound Card Game - Engine Feature Tests
|
|
8
|
+
*
|
|
9
|
+
* Refactored to showcase:
|
|
10
|
+
* ✅ High-level zone utilities (createDeck, bulkMove, drawCards)
|
|
11
|
+
* ✅ Tracker system (hasDrawn)
|
|
12
|
+
* ✅ Standard moves (pass, concede)
|
|
13
|
+
* ✅ Flow context in phase hooks
|
|
14
|
+
* ✅ Massive simplification (10 fields → 4 fields)
|
|
15
|
+
*/
|
|
16
|
+
describe("Riftbound Game - Refactored Engine Features", () => {
|
|
17
|
+
it("should initialize game with ONLY game-specific state", () => {
|
|
18
|
+
const gameDefinition = createMockRiftboundGame();
|
|
19
|
+
const players = createTestPlayers(2);
|
|
20
|
+
const engine = createTestEngine(gameDefinition, players);
|
|
21
|
+
|
|
22
|
+
const state = engine.getState();
|
|
23
|
+
|
|
24
|
+
// ✅ NEW: Only game-specific data
|
|
25
|
+
expect(state.victoryPoints).toBeDefined();
|
|
26
|
+
expect(state.battlefieldControl).toBeDefined();
|
|
27
|
+
expect(state.runePools).toBeDefined();
|
|
28
|
+
expect(state.conqueredThisTurn).toBeDefined();
|
|
29
|
+
|
|
30
|
+
// ✅ REMOVED: No manual tracking
|
|
31
|
+
// @ts-expect-error
|
|
32
|
+
expect(state.phase).toBeUndefined();
|
|
33
|
+
// @ts-expect-error
|
|
34
|
+
expect(state.setupStep).toBeUndefined();
|
|
35
|
+
// @ts-expect-error
|
|
36
|
+
expect(state.turn).toBeUndefined();
|
|
37
|
+
// @ts-expect-error
|
|
38
|
+
expect(state.activePlayer).toBeUndefined();
|
|
39
|
+
// @ts-expect-error
|
|
40
|
+
expect(state.hasDrawnThisTurn).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should have proper zone configuration", () => {
|
|
44
|
+
const gameDefinition = createMockRiftboundGame();
|
|
45
|
+
const zones = gameDefinition.zones;
|
|
46
|
+
|
|
47
|
+
// Verify Riftbound zones
|
|
48
|
+
expect(zones?.mainDeck).toBeDefined();
|
|
49
|
+
expect(zones?.hand).toBeDefined();
|
|
50
|
+
expect(zones?.runeDeck).toBeDefined();
|
|
51
|
+
expect(zones?.runePool).toBeDefined();
|
|
52
|
+
expect(zones?.legendZone).toBeDefined();
|
|
53
|
+
expect(zones?.championZone).toBeDefined();
|
|
54
|
+
expect(zones?.battlefield).toBeDefined();
|
|
55
|
+
expect(zones?.battlefieldRow).toBeDefined();
|
|
56
|
+
expect(zones?.gearArea).toBeDefined();
|
|
57
|
+
expect(zones?.discard).toBeDefined();
|
|
58
|
+
|
|
59
|
+
expect(zones?.mainDeck?.maxSize).toBe(40);
|
|
60
|
+
expect(zones?.runeDeck?.maxSize).toBe(12);
|
|
61
|
+
expect(zones?.legendZone?.maxSize).toBe(1);
|
|
62
|
+
expect(zones?.championZone?.maxSize).toBe(1);
|
|
63
|
+
expect(zones?.battlefieldRow?.maxSize).toBe(3);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should use high-level zone utilities", () => {
|
|
67
|
+
// ✅ NEW: zones.createDeck() for dual deck system
|
|
68
|
+
// ✅ NEW: zones.bulkMove() for rune channeling
|
|
69
|
+
// ✅ NEW: zones.drawCards() for card drawing
|
|
70
|
+
|
|
71
|
+
const gameDefinition = createMockRiftboundGame();
|
|
72
|
+
expect(gameDefinition.moves.initializeDecks).toBeDefined();
|
|
73
|
+
expect(gameDefinition.moves.channelRunes).toBeDefined();
|
|
74
|
+
expect(gameDefinition.moves.drawCard).toBeDefined();
|
|
75
|
+
expect(gameDefinition.moves.drawInitialHand).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should configure tracker system", () => {
|
|
79
|
+
const gameDefinition = createMockRiftboundGame();
|
|
80
|
+
|
|
81
|
+
expect(gameDefinition.trackers).toBeDefined();
|
|
82
|
+
expect(gameDefinition.trackers?.perTurn).toContain("hasDrawn");
|
|
83
|
+
expect(gameDefinition.trackers?.perPlayer).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should use tracker system for draw limitation", () => {
|
|
87
|
+
const gameDefinition = createMockRiftboundGame();
|
|
88
|
+
|
|
89
|
+
const drawCard = gameDefinition.moves.drawCard;
|
|
90
|
+
expect(drawCard.condition).toBeDefined();
|
|
91
|
+
|
|
92
|
+
// Uses context.trackers.check("hasDrawn")
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should use flow context in phase hooks", () => {
|
|
96
|
+
const gameDefinition = createMockRiftboundGame();
|
|
97
|
+
const flow = gameDefinition.flow;
|
|
98
|
+
|
|
99
|
+
// ending phase uses context.getCurrentPlayer()
|
|
100
|
+
expect(flow).toBeDefined();
|
|
101
|
+
if (!(flow && "turn" in flow)) {
|
|
102
|
+
throw new Error("Expected simplified flow definition with turn property");
|
|
103
|
+
}
|
|
104
|
+
expect(flow.turn.phases?.ending).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("should use standard moves", () => {
|
|
108
|
+
const gameDefinition = createMockRiftboundGame();
|
|
109
|
+
|
|
110
|
+
expect(gameDefinition.moves.pass).toBeDefined();
|
|
111
|
+
expect(gameDefinition.moves.concede).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should demonstrate boilerplate reduction", () => {
|
|
115
|
+
// State fields: 10 → 4 (-60%)
|
|
116
|
+
// 593 lines → 440 lines (-26%)
|
|
117
|
+
|
|
118
|
+
const gameDefinition = createMockRiftboundGame();
|
|
119
|
+
const players = createTestPlayers(2);
|
|
120
|
+
const state = gameDefinition.setup(players);
|
|
121
|
+
|
|
122
|
+
expect(Object.keys(state).length).toBe(4);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createPlayerId } from "../types";
|
|
3
|
+
import type {
|
|
4
|
+
ActionDefinition,
|
|
5
|
+
ActionInstance,
|
|
6
|
+
ActionMetadata,
|
|
7
|
+
ActionTiming,
|
|
8
|
+
} from "./action-definition";
|
|
9
|
+
|
|
10
|
+
describe("Action Definition Types", () => {
|
|
11
|
+
describe("ActionTiming", () => {
|
|
12
|
+
it("should define timing constraints with segments", () => {
|
|
13
|
+
const timing: ActionTiming = {
|
|
14
|
+
segments: ["gameplay"],
|
|
15
|
+
phases: ["mainPhase"],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
expect(timing.segments).toEqual(["gameplay"]);
|
|
19
|
+
expect(timing.phases).toEqual(["mainPhase"]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should support custom timing predicates", () => {
|
|
23
|
+
type GameState = { turnCount: number };
|
|
24
|
+
const timing: ActionTiming<GameState> = {
|
|
25
|
+
custom: (state) => state.turnCount > 5,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
expect(timing.custom?.({ turnCount: 6 })).toBe(true);
|
|
29
|
+
expect(timing.custom?.({ turnCount: 3 })).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should support multiple phases and steps", () => {
|
|
33
|
+
const timing: ActionTiming = {
|
|
34
|
+
segments: ["gameplay"],
|
|
35
|
+
phases: ["mainPhase", "combatPhase"],
|
|
36
|
+
steps: ["attackStep", "blockStep"],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(timing.phases).toHaveLength(2);
|
|
40
|
+
expect(timing.steps).toHaveLength(2);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("ActionMetadata", () => {
|
|
45
|
+
it("should define metadata for categorization", () => {
|
|
46
|
+
const metadata: ActionMetadata = {
|
|
47
|
+
category: "card-play",
|
|
48
|
+
subcategory: "creature",
|
|
49
|
+
tags: ["costs-resources", "requires-target"],
|
|
50
|
+
priorityHint: 10,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
expect(metadata.category).toBe("card-play");
|
|
54
|
+
expect(metadata.tags).toContain("costs-resources");
|
|
55
|
+
expect(metadata.priorityHint).toBe(10);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should support hidden actions", () => {
|
|
59
|
+
const metadata: ActionMetadata = {
|
|
60
|
+
hidden: true,
|
|
61
|
+
category: "internal",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
expect(metadata.hidden).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("ActionDefinition", () => {
|
|
69
|
+
it("should define minimal action with just id and name", () => {
|
|
70
|
+
const action: ActionDefinition = {
|
|
71
|
+
id: "pass",
|
|
72
|
+
name: "Pass Priority",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
expect(action.id).toBe("pass");
|
|
76
|
+
expect(action.name).toBe("Pass Priority");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should define action with timing constraints", () => {
|
|
80
|
+
const action: ActionDefinition = {
|
|
81
|
+
id: "play-creature",
|
|
82
|
+
name: "Play Creature",
|
|
83
|
+
description: "Play a creature card from your hand",
|
|
84
|
+
timing: {
|
|
85
|
+
segments: ["gameplay"],
|
|
86
|
+
phases: ["mainPhase"],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
expect(action.timing?.segments).toEqual(["gameplay"]);
|
|
91
|
+
expect(action.timing?.phases).toEqual(["mainPhase"]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should define action with target requirements", () => {
|
|
95
|
+
const action: ActionDefinition = {
|
|
96
|
+
id: "lightning-bolt",
|
|
97
|
+
name: "Lightning Bolt",
|
|
98
|
+
targets: [
|
|
99
|
+
{
|
|
100
|
+
filter: { type: "creature" },
|
|
101
|
+
count: 1,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
expect(action.targets).toHaveLength(1);
|
|
107
|
+
expect(action.targets?.[0].count).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should define action with metadata", () => {
|
|
111
|
+
const action: ActionDefinition = {
|
|
112
|
+
id: "attack",
|
|
113
|
+
name: "Attack",
|
|
114
|
+
metadata: {
|
|
115
|
+
category: "combat",
|
|
116
|
+
tags: ["combat-action"],
|
|
117
|
+
priorityHint: 8,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
expect(action.metadata?.category).toBe("combat");
|
|
122
|
+
expect(action.metadata?.priorityHint).toBe(8);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should support game-specific state types", () => {
|
|
126
|
+
type LorcanaState = { turnPhase: string; inkPool: number };
|
|
127
|
+
|
|
128
|
+
const action: ActionDefinition<LorcanaState> = {
|
|
129
|
+
id: "quest",
|
|
130
|
+
name: "Quest with Character",
|
|
131
|
+
timing: {
|
|
132
|
+
custom: (state) => state.turnPhase === "main" && state.inkPool >= 0,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
expect(action.timing?.custom?.({ turnPhase: "main", inkPool: 3 })).toBe(
|
|
137
|
+
true,
|
|
138
|
+
);
|
|
139
|
+
expect(action.timing?.custom?.({ turnPhase: "draw", inkPool: 3 })).toBe(
|
|
140
|
+
false,
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("ActionInstance", () => {
|
|
146
|
+
it("should create action instance without targets", () => {
|
|
147
|
+
const player1 = createPlayerId("player1");
|
|
148
|
+
const instance: ActionInstance = {
|
|
149
|
+
actionId: "pass",
|
|
150
|
+
playerId: player1,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
expect(instance.actionId).toBe("pass");
|
|
154
|
+
expect(instance.playerId).toBe(player1);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should create action instance with targets", () => {
|
|
158
|
+
const player1 = createPlayerId("player1");
|
|
159
|
+
const instance: ActionInstance = {
|
|
160
|
+
actionId: "lightning-bolt",
|
|
161
|
+
playerId: player1,
|
|
162
|
+
targets: [["card1"]],
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
expect(instance.targets).toHaveLength(1);
|
|
166
|
+
expect(instance.targets?.[0]).toEqual(["card1"]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should create action instance with custom parameters", () => {
|
|
170
|
+
const player1 = createPlayerId("player1");
|
|
171
|
+
const instance: ActionInstance = {
|
|
172
|
+
actionId: "choose-option",
|
|
173
|
+
playerId: player1,
|
|
174
|
+
params: {
|
|
175
|
+
optionIndex: 2,
|
|
176
|
+
cardId: "card1",
|
|
177
|
+
},
|
|
178
|
+
timestamp: Date.now(),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
expect(instance.params?.optionIndex).toBe(2);
|
|
182
|
+
expect(instance.timestamp).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should support multi-target actions", () => {
|
|
186
|
+
const player1 = createPlayerId("player1");
|
|
187
|
+
const instance: ActionInstance = {
|
|
188
|
+
actionId: "distribute-damage",
|
|
189
|
+
playerId: player1,
|
|
190
|
+
targets: [
|
|
191
|
+
["creature1", "creature2"], // First target group
|
|
192
|
+
["player1"], // Second target group
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
expect(instance.targets).toHaveLength(2);
|
|
197
|
+
expect(instance.targets?.[0]).toHaveLength(2);
|
|
198
|
+
expect(instance.targets?.[1]).toHaveLength(1);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { TargetDefinition } from "../targeting/target-definition";
|
|
2
|
+
import type { PlayerId } from "../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Action Timing Constraint
|
|
6
|
+
* Specifies when an action can be performed in terms of game flow.
|
|
7
|
+
*
|
|
8
|
+
* This is a thin layer over core-engine's phase/segment/step system,
|
|
9
|
+
* providing a way to validate timing without duplicating core-engine logic.
|
|
10
|
+
*/
|
|
11
|
+
export type ActionTiming<TGameState = unknown> = {
|
|
12
|
+
/** Segments where this action is allowed (e.g., "setup", "gameplay") */
|
|
13
|
+
segments?: string[];
|
|
14
|
+
|
|
15
|
+
/** Phases where this action is allowed (e.g., "mainPhase", "combatPhase") */
|
|
16
|
+
phases?: string[];
|
|
17
|
+
|
|
18
|
+
/** Steps where this action is allowed (e.g., "drawStep", "playStep") */
|
|
19
|
+
steps?: string[];
|
|
20
|
+
|
|
21
|
+
/** Custom timing predicate for complex game-specific timing rules */
|
|
22
|
+
custom?: (state: TGameState) => boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Action Metadata
|
|
27
|
+
* Provides categorization and UI/logging information for actions.
|
|
28
|
+
* Games can define their own category taxonomies.
|
|
29
|
+
*/
|
|
30
|
+
export type ActionMetadata = {
|
|
31
|
+
/** Category for UI grouping (game-defined, e.g., "card-play", "combat", "special") */
|
|
32
|
+
category?: string;
|
|
33
|
+
|
|
34
|
+
/** Subcategory for finer-grained grouping */
|
|
35
|
+
subcategory?: string;
|
|
36
|
+
|
|
37
|
+
/** Tags for flexible categorization (e.g., ["instant-speed", "costs-resources"]) */
|
|
38
|
+
tags?: string[];
|
|
39
|
+
|
|
40
|
+
/** Priority hint for AI/automation (higher = more important) */
|
|
41
|
+
priorityHint?: number;
|
|
42
|
+
|
|
43
|
+
/** Whether this action is hidden from normal UI (for automatic/internal actions) */
|
|
44
|
+
hidden?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Action Definition
|
|
49
|
+
*
|
|
50
|
+
* A minimal, game-agnostic definition of a player action.
|
|
51
|
+
* This complements core-engine's EnumerableMove by providing:
|
|
52
|
+
* - Timing validation (segments/phases/steps)
|
|
53
|
+
* - Metadata for UI/logging/categorization
|
|
54
|
+
* - Target specifications using @drmxrcy/tcg-core's targeting system
|
|
55
|
+
*
|
|
56
|
+
* The actual execution logic, game-specific constraints, and cost validation
|
|
57
|
+
* are handled by core-engine's EnumerableMove.getConstraints() and execute().
|
|
58
|
+
*/
|
|
59
|
+
export type ActionDefinition<TGameState = unknown> = {
|
|
60
|
+
/** Unique identifier for this action */
|
|
61
|
+
id: string;
|
|
62
|
+
|
|
63
|
+
/** Human-readable name for UI display */
|
|
64
|
+
name: string;
|
|
65
|
+
|
|
66
|
+
/** Optional description for tooltips/help */
|
|
67
|
+
description?: string;
|
|
68
|
+
|
|
69
|
+
/** When this action can be performed */
|
|
70
|
+
timing?: ActionTiming<TGameState>;
|
|
71
|
+
|
|
72
|
+
/** Target requirements using @drmxrcy/tcg-core's targeting system */
|
|
73
|
+
targets?: TargetDefinition[];
|
|
74
|
+
|
|
75
|
+
/** Metadata for categorization and UI */
|
|
76
|
+
metadata?: ActionMetadata;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Action Instance
|
|
81
|
+
*
|
|
82
|
+
* Represents a specific action being performed by a player.
|
|
83
|
+
* This is the bridge between @drmxrcy/tcg-core's action definitions and
|
|
84
|
+
* core-engine's move execution system.
|
|
85
|
+
*/
|
|
86
|
+
export type ActionInstance = {
|
|
87
|
+
/** The action being performed */
|
|
88
|
+
actionId: string;
|
|
89
|
+
|
|
90
|
+
/** Player performing the action */
|
|
91
|
+
playerId: PlayerId;
|
|
92
|
+
|
|
93
|
+
/** Selected targets (array of arrays for multi-target actions) */
|
|
94
|
+
targets?: string[][];
|
|
95
|
+
|
|
96
|
+
/** Additional action-specific parameters */
|
|
97
|
+
params?: Record<string, unknown>;
|
|
98
|
+
|
|
99
|
+
/** Timestamp when action was initiated */
|
|
100
|
+
timestamp?: number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Action Validation Result
|
|
105
|
+
*
|
|
106
|
+
* Result of validating whether an action can be performed.
|
|
107
|
+
* Focused on timing and target validation - cost validation
|
|
108
|
+
* is handled by core-engine's constraint system.
|
|
109
|
+
*/
|
|
110
|
+
export type ActionValidationResult = {
|
|
111
|
+
/** Whether the action is valid */
|
|
112
|
+
valid: boolean;
|
|
113
|
+
|
|
114
|
+
/** Human-readable error message if invalid */
|
|
115
|
+
error?: string;
|
|
116
|
+
|
|
117
|
+
/** Specific reason code for programmatic handling */
|
|
118
|
+
reason?: "timing" | "targets" | "precondition";
|
|
119
|
+
|
|
120
|
+
/** Invalid target indices (if reason is "targets") */
|
|
121
|
+
invalidTargets?: number[];
|
|
122
|
+
};
|