@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,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages game move history with Immer-based immutable store.
|
|
5
|
+
* Tracks all move executions chronologically with player-aware visibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { nanoid } from "nanoid";
|
|
9
|
+
import type { PlayerId } from "../types/branded";
|
|
10
|
+
import type {
|
|
11
|
+
FormattedHistoryEntry,
|
|
12
|
+
HistoryEntry,
|
|
13
|
+
HistoryQueryOptions,
|
|
14
|
+
MessageTemplateData,
|
|
15
|
+
VerbosityLevel,
|
|
16
|
+
VerbosityMessages,
|
|
17
|
+
} from "./types";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* History Manager Options
|
|
21
|
+
*
|
|
22
|
+
* Configuration for HistoryManager initialization.
|
|
23
|
+
*/
|
|
24
|
+
export type HistoryManagerOptions = {
|
|
25
|
+
/** Initial entries (for replay/restore) */
|
|
26
|
+
initialEntries?: HistoryEntry[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* History Manager
|
|
31
|
+
*
|
|
32
|
+
* In-memory store for game move history.
|
|
33
|
+
* Uses simple array storage (Immer patches handle immutability at engine level).
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const manager = new HistoryManager();
|
|
38
|
+
*
|
|
39
|
+
* // Add entry
|
|
40
|
+
* manager.addEntry({
|
|
41
|
+
* moveId: 'playCard',
|
|
42
|
+
* playerId: 'p1',
|
|
43
|
+
* params: { cardId: 'card-123' },
|
|
44
|
+
* timestamp: Date.now(),
|
|
45
|
+
* success: true,
|
|
46
|
+
* messages: {
|
|
47
|
+
* visibility: 'PUBLIC',
|
|
48
|
+
* messages: {
|
|
49
|
+
* casual: { key: 'moves.playCard', values: { card: 'Knight' } }
|
|
50
|
+
* }
|
|
51
|
+
* }
|
|
52
|
+
* });
|
|
53
|
+
*
|
|
54
|
+
* // Query history
|
|
55
|
+
* const entries = manager.query({ playerId: 'p1', verbosity: 'CASUAL' });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export class HistoryManager {
|
|
59
|
+
private entries: HistoryEntry[];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a new HistoryManager
|
|
63
|
+
*
|
|
64
|
+
* @param options - Configuration options
|
|
65
|
+
*/
|
|
66
|
+
constructor(options: HistoryManagerOptions = {}) {
|
|
67
|
+
this.entries = options.initialEntries ?? [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Add a new history entry
|
|
72
|
+
*
|
|
73
|
+
* @param entry - Entry data (without id - will be generated)
|
|
74
|
+
* @returns The created entry with generated id
|
|
75
|
+
*/
|
|
76
|
+
addEntry(entry: Omit<HistoryEntry, "id">): HistoryEntry {
|
|
77
|
+
const fullEntry: HistoryEntry = {
|
|
78
|
+
id: nanoid(),
|
|
79
|
+
...entry,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.entries.push(fullEntry);
|
|
83
|
+
return fullEntry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Query history entries with filtering and formatting
|
|
88
|
+
*
|
|
89
|
+
* @param options - Query options
|
|
90
|
+
* @returns Formatted entries matching the query
|
|
91
|
+
*/
|
|
92
|
+
query(options: HistoryQueryOptions = {}): FormattedHistoryEntry[] {
|
|
93
|
+
const {
|
|
94
|
+
playerId,
|
|
95
|
+
verbosity = "CASUAL",
|
|
96
|
+
since,
|
|
97
|
+
moveId,
|
|
98
|
+
includeSuccess = true,
|
|
99
|
+
includeFailures = true,
|
|
100
|
+
} = options;
|
|
101
|
+
|
|
102
|
+
// Filter entries
|
|
103
|
+
let filtered = this.entries;
|
|
104
|
+
|
|
105
|
+
// Filter by timestamp
|
|
106
|
+
if (since !== undefined) {
|
|
107
|
+
filtered = filtered.filter((entry) => entry.timestamp > since);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Filter by move ID
|
|
111
|
+
if (moveId !== undefined) {
|
|
112
|
+
filtered = filtered.filter((entry) => entry.moveId === moveId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Filter by success/failure
|
|
116
|
+
if (!includeSuccess) {
|
|
117
|
+
filtered = filtered.filter((entry) => !entry.success);
|
|
118
|
+
}
|
|
119
|
+
if (!includeFailures) {
|
|
120
|
+
filtered = filtered.filter((entry) => entry.success);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Filter by player visibility and format
|
|
124
|
+
return filtered
|
|
125
|
+
.map((entry) => this.formatEntry(entry, playerId, verbosity))
|
|
126
|
+
.filter((entry): entry is FormattedHistoryEntry => entry !== null);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get all entries (raw, no filtering)
|
|
131
|
+
*
|
|
132
|
+
* Used for debugging and serialization.
|
|
133
|
+
*
|
|
134
|
+
* @returns All history entries
|
|
135
|
+
*/
|
|
136
|
+
getAllEntries(): readonly HistoryEntry[] {
|
|
137
|
+
return this.entries;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clear all history entries
|
|
142
|
+
*
|
|
143
|
+
* Used for testing.
|
|
144
|
+
*/
|
|
145
|
+
clear(): void {
|
|
146
|
+
this.entries = [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get the number of entries
|
|
151
|
+
*
|
|
152
|
+
* @returns Entry count
|
|
153
|
+
*/
|
|
154
|
+
getCount(): number {
|
|
155
|
+
return this.entries.length;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Check if player can see this entry
|
|
160
|
+
*
|
|
161
|
+
* @param entry - History entry
|
|
162
|
+
* @param playerId - Player requesting access (undefined = see all)
|
|
163
|
+
* @returns True if player can see this entry
|
|
164
|
+
*/
|
|
165
|
+
private canPlayerSeeEntry(
|
|
166
|
+
entry: HistoryEntry,
|
|
167
|
+
playerId: PlayerId | undefined,
|
|
168
|
+
): boolean {
|
|
169
|
+
// If no player filter, show all entries
|
|
170
|
+
if (playerId === undefined) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const { messages } = entry;
|
|
175
|
+
|
|
176
|
+
// PUBLIC entries are visible to all
|
|
177
|
+
if (messages.visibility === "PUBLIC") {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// PRIVATE entries are visible only to specified players
|
|
182
|
+
if (messages.visibility === "PRIVATE") {
|
|
183
|
+
return (
|
|
184
|
+
messages.visibleTo === undefined ||
|
|
185
|
+
messages.visibleTo.some((id) => String(id) === String(playerId))
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// PLAYER_SPECIFIC entries are visible if player has a message
|
|
190
|
+
if (messages.visibility === "PLAYER_SPECIFIC") {
|
|
191
|
+
return String(playerId) in messages.messages;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Format an entry for display
|
|
199
|
+
*
|
|
200
|
+
* @param entry - Raw history entry
|
|
201
|
+
* @param playerId - Player requesting the entry (for filtering)
|
|
202
|
+
* @param verbosity - Verbosity level for message selection
|
|
203
|
+
* @returns Formatted entry or null if player can't see it
|
|
204
|
+
*/
|
|
205
|
+
private formatEntry(
|
|
206
|
+
entry: HistoryEntry,
|
|
207
|
+
playerId: PlayerId | undefined,
|
|
208
|
+
verbosity: VerbosityLevel,
|
|
209
|
+
): FormattedHistoryEntry | null {
|
|
210
|
+
// Check visibility
|
|
211
|
+
if (!this.canPlayerSeeEntry(entry, playerId)) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Get message for this player and verbosity
|
|
216
|
+
const message = this.getMessageForPlayer(entry, playerId, verbosity);
|
|
217
|
+
if (!message) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Build formatted entry
|
|
222
|
+
const formatted: FormattedHistoryEntry = {
|
|
223
|
+
id: entry.id,
|
|
224
|
+
moveId: entry.moveId,
|
|
225
|
+
playerId: entry.playerId,
|
|
226
|
+
timestamp: entry.timestamp,
|
|
227
|
+
turn: entry.turn,
|
|
228
|
+
phase: entry.phase,
|
|
229
|
+
segment: entry.segment,
|
|
230
|
+
message,
|
|
231
|
+
success: entry.success,
|
|
232
|
+
error: entry.error,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// Include additional details for DEVELOPER mode
|
|
236
|
+
if (verbosity === "DEVELOPER") {
|
|
237
|
+
formatted.metadata = entry.metadata;
|
|
238
|
+
formatted.params = entry.params;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return formatted;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get the appropriate message for a player and verbosity level
|
|
246
|
+
*
|
|
247
|
+
* @param entry - History entry
|
|
248
|
+
* @param playerId - Player requesting the message
|
|
249
|
+
* @param verbosity - Verbosity level
|
|
250
|
+
* @returns Message template data or null if no message available
|
|
251
|
+
*/
|
|
252
|
+
private getMessageForPlayer(
|
|
253
|
+
entry: HistoryEntry,
|
|
254
|
+
playerId: PlayerId | undefined,
|
|
255
|
+
verbosity: VerbosityLevel,
|
|
256
|
+
): MessageTemplateData | null {
|
|
257
|
+
const { messages } = entry;
|
|
258
|
+
|
|
259
|
+
// Get verbosity-specific messages
|
|
260
|
+
let verbosityMessages: VerbosityMessages | undefined;
|
|
261
|
+
|
|
262
|
+
if (messages.visibility === "PUBLIC" || messages.visibility === "PRIVATE") {
|
|
263
|
+
verbosityMessages = messages.messages;
|
|
264
|
+
} else if (messages.visibility === "PLAYER_SPECIFIC") {
|
|
265
|
+
// Get player-specific messages
|
|
266
|
+
if (playerId !== undefined) {
|
|
267
|
+
verbosityMessages = messages.messages[String(playerId)];
|
|
268
|
+
} else {
|
|
269
|
+
// If no player specified, use first available
|
|
270
|
+
const firstPlayerId = Object.keys(messages.messages)[0];
|
|
271
|
+
if (firstPlayerId) {
|
|
272
|
+
verbosityMessages = messages.messages[firstPlayerId];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!verbosityMessages) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Select message based on verbosity level with fallback
|
|
282
|
+
return this.selectMessageByVerbosity(verbosityMessages, verbosity);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Select message by verbosity level with fallback
|
|
287
|
+
*
|
|
288
|
+
* Falls back to less detailed level if requested level not available:
|
|
289
|
+
* DEVELOPER -> ADVANCED -> CASUAL
|
|
290
|
+
*
|
|
291
|
+
* @param messages - Verbosity-specific messages
|
|
292
|
+
* @param verbosity - Requested verbosity level
|
|
293
|
+
* @returns Message template data or null if no messages available
|
|
294
|
+
*/
|
|
295
|
+
private selectMessageByVerbosity(
|
|
296
|
+
messages: VerbosityMessages,
|
|
297
|
+
verbosity: VerbosityLevel,
|
|
298
|
+
): MessageTemplateData | null {
|
|
299
|
+
switch (verbosity) {
|
|
300
|
+
case "DEVELOPER":
|
|
301
|
+
return (
|
|
302
|
+
messages.developer ?? messages.advanced ?? messages.casual ?? null
|
|
303
|
+
);
|
|
304
|
+
case "ADVANCED":
|
|
305
|
+
return messages.advanced ?? messages.casual ?? null;
|
|
306
|
+
case "CASUAL":
|
|
307
|
+
return messages.casual ?? null;
|
|
308
|
+
default:
|
|
309
|
+
return messages.casual ?? null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History Operations
|
|
3
|
+
*
|
|
4
|
+
* Operations API exposed to move reducers via MoveContext.
|
|
5
|
+
* Allows moves to log custom history entries with player-specific visibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PlayerId } from "../types/branded";
|
|
9
|
+
import type { HistoryManager } from "./history-manager";
|
|
10
|
+
import type { HistoryMessages } from "./types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* History Operations
|
|
14
|
+
*
|
|
15
|
+
* API for logging history entries from within move reducers.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* // In a move reducer
|
|
20
|
+
* reducer: (draft, context) => {
|
|
21
|
+
* // Log a public message
|
|
22
|
+
* context.history.log({
|
|
23
|
+
* messages: {
|
|
24
|
+
* visibility: 'PUBLIC',
|
|
25
|
+
* messages: {
|
|
26
|
+
* casual: { key: 'moves.draw', values: { count: 5 } },
|
|
27
|
+
* advanced: { key: 'moves.draw.detailed', values: { cardIds: [...] } }
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Log a player-specific message (mulligan)
|
|
33
|
+
* context.history.log({
|
|
34
|
+
* messages: {
|
|
35
|
+
* visibility: 'PLAYER_SPECIFIC',
|
|
36
|
+
* messages: {
|
|
37
|
+
* [playerId]: {
|
|
38
|
+
* casual: { key: 'mulligan.self', values: { cards: [...] } }
|
|
39
|
+
* },
|
|
40
|
+
* [opponentId]: {
|
|
41
|
+
* casual: { key: 'mulligan.opponent', values: { count: 3 } }
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* });
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export type HistoryOperations = {
|
|
50
|
+
/**
|
|
51
|
+
* Log a custom history entry
|
|
52
|
+
*
|
|
53
|
+
* Creates a history entry with custom messages.
|
|
54
|
+
* Used by move reducers to add detailed logging beyond the automatic entry.
|
|
55
|
+
*
|
|
56
|
+
* Note: The engine automatically creates a base history entry for each move.
|
|
57
|
+
* Use this method to add additional context or player-specific details.
|
|
58
|
+
*
|
|
59
|
+
* @param input - Message templates and metadata
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // Log card draw with player-specific visibility
|
|
64
|
+
* context.history.log({
|
|
65
|
+
* messages: {
|
|
66
|
+
* visibility: 'PLAYER_SPECIFIC',
|
|
67
|
+
* messages: {
|
|
68
|
+
* [playerId]: {
|
|
69
|
+
* casual: { key: 'draw.self', values: { cards: ['Knight', 'Wizard'] } }
|
|
70
|
+
* },
|
|
71
|
+
* [opponentId]: {
|
|
72
|
+
* casual: { key: 'draw.opponent', values: { count: 2 } }
|
|
73
|
+
* }
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
log(input: {
|
|
80
|
+
messages: HistoryMessages;
|
|
81
|
+
metadata?: Record<string, unknown>;
|
|
82
|
+
}): void;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create History Operations
|
|
87
|
+
*
|
|
88
|
+
* Factory for creating HistoryOperations bound to a specific context.
|
|
89
|
+
*
|
|
90
|
+
* @param manager - History manager instance
|
|
91
|
+
* @param context - Move execution context
|
|
92
|
+
* @returns History operations API
|
|
93
|
+
*/
|
|
94
|
+
export function createHistoryOperations(
|
|
95
|
+
manager: HistoryManager,
|
|
96
|
+
context: {
|
|
97
|
+
moveId: string;
|
|
98
|
+
playerId: string;
|
|
99
|
+
params: unknown;
|
|
100
|
+
timestamp: number;
|
|
101
|
+
turn?: number;
|
|
102
|
+
phase?: string;
|
|
103
|
+
segment?: string;
|
|
104
|
+
},
|
|
105
|
+
): HistoryOperations {
|
|
106
|
+
return {
|
|
107
|
+
log(input) {
|
|
108
|
+
manager.addEntry({
|
|
109
|
+
moveId: context.moveId,
|
|
110
|
+
playerId: context.playerId as PlayerId,
|
|
111
|
+
params: context.params,
|
|
112
|
+
timestamp: context.timestamp,
|
|
113
|
+
turn: context.turn,
|
|
114
|
+
phase: context.phase,
|
|
115
|
+
segment: context.segment,
|
|
116
|
+
success: true,
|
|
117
|
+
messages: input.messages,
|
|
118
|
+
metadata: input.metadata,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the game move history system.
|
|
5
|
+
* Provides structured move logging with player-aware visibility and i18next localization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PlayerId } from "../types/branded";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Verbosity Level
|
|
12
|
+
*
|
|
13
|
+
* Controls the level of detail in history messages:
|
|
14
|
+
* - CASUAL: Simple, narrative descriptions for casual players
|
|
15
|
+
* - ADVANCED: Technical details for competitive players
|
|
16
|
+
* - DEVELOPER: Full internal details for debugging
|
|
17
|
+
*/
|
|
18
|
+
export type VerbosityLevel = "CASUAL" | "ADVANCED" | "DEVELOPER";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Visibility Level
|
|
22
|
+
*
|
|
23
|
+
* Controls who can see a history entry:
|
|
24
|
+
* - PUBLIC: Visible to all players
|
|
25
|
+
* - PRIVATE: Visible only to specific player(s)
|
|
26
|
+
* - PLAYER_SPECIFIC: Different details shown to different players
|
|
27
|
+
*/
|
|
28
|
+
export type VisibilityLevel = "PUBLIC" | "PRIVATE" | "PLAYER_SPECIFIC";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Message Template Data
|
|
32
|
+
*
|
|
33
|
+
* i18next-compatible message template with interpolation values.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* // Template: "{{player}} drew {{count}} cards"
|
|
38
|
+
* {
|
|
39
|
+
* key: "moves.draw.success",
|
|
40
|
+
* values: { player: "Player 1", count: 5 }
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export type MessageTemplateData = {
|
|
45
|
+
/** i18next translation key */
|
|
46
|
+
key: string;
|
|
47
|
+
|
|
48
|
+
/** Interpolation values for the template */
|
|
49
|
+
values?: Record<string, unknown>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Verbosity-Specific Messages
|
|
54
|
+
*
|
|
55
|
+
* Different message templates for different verbosity levels.
|
|
56
|
+
* All levels are optional; if not provided, falls back to less detailed level.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* {
|
|
61
|
+
* casual: { key: "moves.mulligan.casual", values: { count: 3 } },
|
|
62
|
+
* advanced: { key: "moves.mulligan.advanced", values: { cardIds: [...] } },
|
|
63
|
+
* developer: { key: "moves.mulligan.dev", values: { fullContext: {...} } }
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export type VerbosityMessages = {
|
|
68
|
+
/** Message for casual players (simple, narrative) */
|
|
69
|
+
casual?: MessageTemplateData;
|
|
70
|
+
|
|
71
|
+
/** Message for advanced players (technical details) */
|
|
72
|
+
advanced?: MessageTemplateData;
|
|
73
|
+
|
|
74
|
+
/** Message for developers (full internal details) */
|
|
75
|
+
developer?: MessageTemplateData;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Player-Specific Message Data
|
|
80
|
+
*
|
|
81
|
+
* Different message templates shown to different players.
|
|
82
|
+
* Used for moves with private information (e.g., opponent mulligans).
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* // Mulligan: owning player sees cards, opponent sees count only
|
|
87
|
+
* {
|
|
88
|
+
* player_one: {
|
|
89
|
+
* casual: { key: "mulligan.self", values: { cards: ["card1", "card2"] } }
|
|
90
|
+
* },
|
|
91
|
+
* player_two: {
|
|
92
|
+
* casual: { key: "mulligan.opponent", values: { count: 2 } }
|
|
93
|
+
* }
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export type PlayerSpecificMessages = Record<string, VerbosityMessages>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* History Entry Messages
|
|
101
|
+
*
|
|
102
|
+
* Message templates for a history entry.
|
|
103
|
+
* Can be public (same for all) or player-specific (different per player).
|
|
104
|
+
*/
|
|
105
|
+
export type HistoryMessages =
|
|
106
|
+
| {
|
|
107
|
+
visibility: "PUBLIC" | "PRIVATE";
|
|
108
|
+
/** Messages at different verbosity levels */
|
|
109
|
+
messages: VerbosityMessages;
|
|
110
|
+
/** If PRIVATE, which player(s) can see this entry */
|
|
111
|
+
visibleTo?: PlayerId[];
|
|
112
|
+
}
|
|
113
|
+
| {
|
|
114
|
+
visibility: "PLAYER_SPECIFIC";
|
|
115
|
+
/** Different messages for different players */
|
|
116
|
+
messages: PlayerSpecificMessages;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* History Entry
|
|
121
|
+
*
|
|
122
|
+
* A single entry in the game move history.
|
|
123
|
+
* Contains all information about a move execution including messages,
|
|
124
|
+
* visibility, and raw data for debugging.
|
|
125
|
+
*/
|
|
126
|
+
export type HistoryEntry = {
|
|
127
|
+
/** Unique identifier for this entry */
|
|
128
|
+
id: string;
|
|
129
|
+
|
|
130
|
+
/** Move ID that was executed */
|
|
131
|
+
moveId: string;
|
|
132
|
+
|
|
133
|
+
/** Player who executed the move */
|
|
134
|
+
playerId: PlayerId;
|
|
135
|
+
|
|
136
|
+
/** Move-specific parameters */
|
|
137
|
+
params: unknown;
|
|
138
|
+
|
|
139
|
+
/** Timestamp when move was executed (milliseconds) */
|
|
140
|
+
timestamp: number;
|
|
141
|
+
|
|
142
|
+
/** Turn number when move was executed */
|
|
143
|
+
turn?: number;
|
|
144
|
+
|
|
145
|
+
/** Game phase when move was executed */
|
|
146
|
+
phase?: string;
|
|
147
|
+
|
|
148
|
+
/** Game segment when move was executed */
|
|
149
|
+
segment?: string;
|
|
150
|
+
|
|
151
|
+
/** Message templates for this entry */
|
|
152
|
+
messages: HistoryMessages;
|
|
153
|
+
|
|
154
|
+
/** Whether the move was successful */
|
|
155
|
+
success: boolean;
|
|
156
|
+
|
|
157
|
+
/** Error information if move failed */
|
|
158
|
+
error?: {
|
|
159
|
+
/** Error code */
|
|
160
|
+
code: string;
|
|
161
|
+
/** Error message */
|
|
162
|
+
message: string;
|
|
163
|
+
/** Additional error context */
|
|
164
|
+
context?: Record<string, unknown>;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** Additional metadata for debugging */
|
|
168
|
+
metadata?: Record<string, unknown>;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Formatted History Entry
|
|
173
|
+
*
|
|
174
|
+
* A history entry with formatted message ready for display.
|
|
175
|
+
* Returned by getHistory() after filtering and formatting.
|
|
176
|
+
*/
|
|
177
|
+
export type FormattedHistoryEntry = {
|
|
178
|
+
/** Unique identifier for this entry */
|
|
179
|
+
id: string;
|
|
180
|
+
|
|
181
|
+
/** Move ID that was executed */
|
|
182
|
+
moveId: string;
|
|
183
|
+
|
|
184
|
+
/** Player who executed the move */
|
|
185
|
+
playerId: PlayerId;
|
|
186
|
+
|
|
187
|
+
/** Timestamp when move was executed (milliseconds) */
|
|
188
|
+
timestamp: number;
|
|
189
|
+
|
|
190
|
+
/** Turn number when move was executed */
|
|
191
|
+
turn?: number;
|
|
192
|
+
|
|
193
|
+
/** Game phase when move was executed */
|
|
194
|
+
phase?: string;
|
|
195
|
+
|
|
196
|
+
/** Game segment when move was executed */
|
|
197
|
+
segment?: string;
|
|
198
|
+
|
|
199
|
+
/** Formatted message ready for display */
|
|
200
|
+
message: MessageTemplateData;
|
|
201
|
+
|
|
202
|
+
/** Whether the move was successful */
|
|
203
|
+
success: boolean;
|
|
204
|
+
|
|
205
|
+
/** Error information if move failed */
|
|
206
|
+
error?: {
|
|
207
|
+
code: string;
|
|
208
|
+
message: string;
|
|
209
|
+
context?: Record<string, unknown>;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** Additional metadata (only included in DEVELOPER mode) */
|
|
213
|
+
metadata?: Record<string, unknown>;
|
|
214
|
+
|
|
215
|
+
/** Raw parameters (only included in DEVELOPER mode) */
|
|
216
|
+
params?: unknown;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* History Query Options
|
|
221
|
+
*
|
|
222
|
+
* Options for querying game history.
|
|
223
|
+
*/
|
|
224
|
+
export type HistoryQueryOptions = {
|
|
225
|
+
/** Filter to entries visible to this player (undefined = all entries) */
|
|
226
|
+
playerId?: PlayerId;
|
|
227
|
+
|
|
228
|
+
/** Verbosity level for message formatting */
|
|
229
|
+
verbosity?: VerbosityLevel;
|
|
230
|
+
|
|
231
|
+
/** Only return entries after this timestamp */
|
|
232
|
+
since?: number;
|
|
233
|
+
|
|
234
|
+
/** Only return entries for this move ID */
|
|
235
|
+
moveId?: string;
|
|
236
|
+
|
|
237
|
+
/** Include successful moves (default: true) */
|
|
238
|
+
includeSuccess?: boolean;
|
|
239
|
+
|
|
240
|
+
/** Include failed moves (default: true) */
|
|
241
|
+
includeFailures?: boolean;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Log Entry Input
|
|
246
|
+
*
|
|
247
|
+
* Input data for creating a history entry via context.history.log()
|
|
248
|
+
*/
|
|
249
|
+
export type LogEntryInput = {
|
|
250
|
+
/** Message templates for this entry */
|
|
251
|
+
messages: HistoryMessages;
|
|
252
|
+
|
|
253
|
+
/** Additional metadata for debugging */
|
|
254
|
+
metadata?: Record<string, unknown>;
|
|
255
|
+
};
|