p2p-lockstep-kit-session 0.1.2 → 0.1.4

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.
@@ -0,0 +1,363 @@
1
+ import { NetworkClient } from 'p2p-lockstep-kit-network';
2
+
3
+ type SessionMessageType = 'READY' | 'START' | 'MOVE' | 'UNDO' | 'RESTART' | 'APPROVE' | 'REJECT' | 'REJOIN' | 'SYNC_REQUEST' | 'SYNC_STATE' | 'OFFLINE' | 'ONLINE' | 'GAME_OVER';
4
+ type SessionMessage = {
5
+ type: SessionMessageType;
6
+ from?: string;
7
+ seq?: number;
8
+ sid?: string;
9
+ turn?: number;
10
+ stateHash?: string;
11
+ payload?: any;
12
+ };
13
+
14
+ type CommandOrigin = 'local' | 'remote';
15
+ type BusMessageType = SessionMessageType | 'OFFLINE' | 'ONLINE' | 'GAME_OVER';
16
+ type BusMessage = Omit<SessionMessage, 'type'> & {
17
+ type: BusMessageType;
18
+ };
19
+ type CommandListener = (message: BusMessage) => Promise<void> | void;
20
+ declare class CommandBus {
21
+ private handlers;
22
+ private processingQueue;
23
+ emit(type: BusMessageType, payload?: unknown, from?: CommandOrigin): void;
24
+ register(type: BusMessageType, handler: CommandListener): void;
25
+ dispatch(message: BusMessage): void;
26
+ }
27
+
28
+ /**
29
+ * Network client wrapper that bridges NetworkClient with CommandBus
30
+ * Handles message encoding/decoding and connection state monitoring
31
+ */
32
+ declare class NetClient {
33
+ private readonly client;
34
+ private readonly bus;
35
+ private localPeerId;
36
+ private remotePeerId;
37
+ private isConnected;
38
+ private connectionChangeListener;
39
+ private mediaStateListener;
40
+ constructor(client: NetworkClient, bus: CommandBus, peerId: string | null);
41
+ /**
42
+ * Send a message to the remote peer
43
+ * Drops message if not connected and logs warning
44
+ */
45
+ send(message: SessionMessage): void;
46
+ /**
47
+ * Update local and remote peer IDs
48
+ */
49
+ setPeerIds(ids: {
50
+ local?: string | null;
51
+ remote?: string | null;
52
+ }): void;
53
+ /**
54
+ * Get current peer IDs
55
+ */
56
+ getPeerIds(): {
57
+ local: string | null;
58
+ remote: string | null;
59
+ };
60
+ /**
61
+ * Check if currently connected to peer
62
+ */
63
+ getIsConnected(): boolean;
64
+ /**
65
+ * Monitor connection state changes
66
+ * @param handler Called when connection state changes (true=connected, false=disconnected)
67
+ */
68
+ onConnectionChange(handler: (isConnected: boolean) => void): void;
69
+ /**
70
+ * Monitor remote media stream state changes
71
+ * @param handler Called when remote media becomes available or unavailable
72
+ */
73
+ onMediaStateChange(handler: (active: boolean) => void): void;
74
+ }
75
+
76
+ type SessionState = 'idle' | 'ready' | 'could_start' | 'turn' | 'remote_turn' | 'approving' | 'waiting_approval' | 'syncing' | 'offline';
77
+ type SessionEvent = 'REMOTE_READY' | 'READY' | 'START' | 'REMOTE_START' | 'MOVE' | 'REMOTE_MOVE' | 'UNDO' | 'REMOTE_UNDO' | 'RESTART' | 'REMOTE_RESTART' | 'APPROVE' | 'REJECT' | 'GAME_OVER' | 'REJOIN' | 'SYNC' | 'SYNC_COMPLETE' | 'OFFLINE' | 'ONLINE';
78
+
79
+ interface GameStateSnapshot {
80
+ localState: SessionState;
81
+ remoteState: SessionState;
82
+ turn: number;
83
+ history: TurnEntry[];
84
+ lastStart: PlayerLabel | null;
85
+ pendingAction: 'undo' | 'restart' | null;
86
+ connected: boolean;
87
+ }
88
+ interface GameEvent {
89
+ type: 'READY' | 'START' | 'MOVE' | 'GAME_OVER' | 'UNDO' | 'RESTART' | 'OFFLINE' | 'ONLINE' | 'SYNC' | 'ERROR';
90
+ payload?: any;
91
+ from?: 'local' | 'remote';
92
+ timestamp?: number;
93
+ }
94
+ interface IGameObserver {
95
+ onStateChange(snapshot: GameStateSnapshot): void;
96
+ onGameEvent(event: GameEvent): void;
97
+ onConnectionChange?(connected: boolean): void;
98
+ onError?(error: {
99
+ message: string;
100
+ context?: any;
101
+ }): void;
102
+ }
103
+ interface IStateObserver {
104
+ onStateChanged?(): void;
105
+ onHistoryChanged?(): void;
106
+ onGameReset?(): void;
107
+ }
108
+ interface IGamePlugin {
109
+ validateMove(move: unknown, gameState: GameState): ValidationResult;
110
+ checkWin(gameState: GameState, history: TurnEntry[]): PlayerLabel | null;
111
+ initialize?(): void;
112
+ cleanup?(): void;
113
+ }
114
+ interface ValidationResult {
115
+ valid: boolean;
116
+ reason?: string;
117
+ }
118
+ interface GameState {
119
+ history: TurnEntry[];
120
+ localState: 'turn' | 'remote_turn' | string;
121
+ remoteState: 'turn' | 'remote_turn' | string;
122
+ turn: number;
123
+ lastStart: PlayerLabel | null;
124
+ }
125
+ declare class DefaultGamePlugin implements IGamePlugin {
126
+ validateMove(): ValidationResult;
127
+ checkWin(): PlayerLabel | null;
128
+ }
129
+ declare class StateObserverManager {
130
+ private observers;
131
+ subscribe(observer: IStateObserver): void;
132
+ unsubscribe(observer: IStateObserver): void;
133
+ notifyStateChanged(): void;
134
+ notifyHistoryChanged(): void;
135
+ notifyGameReset(): void;
136
+ }
137
+ declare class GameStateObserver {
138
+ private observers;
139
+ private currentSnapshot;
140
+ subscribe(observer: IGameObserver): () => void;
141
+ unsubscribe(observer: IGameObserver): void;
142
+ notifyStateChange(snapshot: GameStateSnapshot): void;
143
+ notifyGameEvent(event: GameEvent): void;
144
+ notifyConnectionChange(connected: boolean): void;
145
+ notifyError(error: {
146
+ message: string;
147
+ context?: any;
148
+ }): void;
149
+ getSnapshot(): GameStateSnapshot | null;
150
+ getObserverCount(): number;
151
+ }
152
+ declare function buildGameStateSnapshot(state: State, connected?: boolean): GameStateSnapshot;
153
+ declare class UINotificationAdapter implements IStateObserver {
154
+ private stateRef;
155
+ private uiObserver;
156
+ private getConnected;
157
+ private lastNotificationTime;
158
+ private notificationThrottleMs;
159
+ constructor(stateRef: State, uiObserver: GameStateObserver, getConnected?: () => boolean);
160
+ onStateChanged(): void;
161
+ onHistoryChanged(): void;
162
+ onGameReset(): void;
163
+ emitEvent(event: Omit<GameEvent, 'timestamp'>): void;
164
+ }
165
+
166
+ type TurnEntry = {
167
+ turn: number;
168
+ player: 'local' | 'remote';
169
+ move?: any;
170
+ };
171
+ type PlayerLabel = 'local' | 'remote';
172
+ declare class State {
173
+ private local;
174
+ private remote;
175
+ private readonly localId;
176
+ private remoteId;
177
+ private readonly history;
178
+ private pendingAction;
179
+ private pendingUndoCount;
180
+ private resumeTurn;
181
+ private lastStart;
182
+ private gamePlugin;
183
+ private stateObserverManager;
184
+ constructor(id: string | null, remoteId: string | null);
185
+ /**
186
+ * Register an internal observer (like plugin pattern)
187
+ * Use this to connect State mutations to UI updates
188
+ */
189
+ subscribeStateObserver(observer: IStateObserver): void;
190
+ getId(): string | null;
191
+ getremoteId(): string | null;
192
+ setremoteId(id: string): void;
193
+ getState(player: PlayerLabel): SessionState;
194
+ getTurnCount(): number;
195
+ getHistory(): TurnEntry[];
196
+ replaceHistory(entries: TurnEntry[]): void;
197
+ clearHistory(): void;
198
+ pushHistory(entry: TurnEntry): void;
199
+ popHistory(): TurnEntry | null;
200
+ canAction(player: PlayerLabel, action: SessionEvent): boolean;
201
+ /**
202
+ * Dispatch an action and automatically determine target state if unique
203
+ * Only use explicit 'to' parameter for ambiguous transitions (APPROVE, REJECT, etc.)
204
+ *
205
+ * For most actions (READY, MOVE, START, etc.), there's only one valid transition,
206
+ * so we automatically find and apply it.
207
+ */
208
+ dispatch(player: PlayerLabel, action: SessionEvent, to?: SessionState): void;
209
+ setPendingAction(action: 'undo' | 'restart' | null): void;
210
+ getPendingAction(): "undo" | "restart" | null;
211
+ setPendingUndoCount(count: 1 | 2 | null): void;
212
+ getPendingUndoCount(): 1 | 2 | null;
213
+ setLastStart(player: PlayerLabel | null): void;
214
+ getLastStart(): PlayerLabel | null;
215
+ setResumeTurn(player: PlayerLabel | null): void;
216
+ getResumeTurn(): PlayerLabel | null;
217
+ private getPlayerFsm;
218
+ /**
219
+ * Save game state snapshot for undo/restart operations
220
+ */
221
+ private gameSnapshot;
222
+ saveGameSnapshot(snapshot: unknown): void;
223
+ getGameSnapshot(): unknown;
224
+ clearGameSnapshot(): void;
225
+ /**
226
+ * Check if there's a pending action (undo/restart)
227
+ */
228
+ hasPendingAction(): boolean;
229
+ /**
230
+ * Clear all pending states (called after approval/rejection)
231
+ */
232
+ clearPendingStates(): void;
233
+ /**
234
+ * Initialize undo request with undo count and current turn holder
235
+ */
236
+ initializeUndoRequest(undoCount: 1 | 2, resumeTurn: PlayerLabel): void;
237
+ /**
238
+ * Initialize restart request with resume turn
239
+ */
240
+ initializeRestartRequest(resumeTurn: PlayerLabel): void;
241
+ /**
242
+ * Check if pending action is undo
243
+ */
244
+ isPendingUndo(): boolean;
245
+ /**
246
+ * Check if pending action is restart
247
+ */
248
+ isPendingRestart(): boolean;
249
+ /**
250
+ * Apply undo by popping history N times
251
+ */
252
+ applyUndo(count?: 1 | 2): void;
253
+ /**
254
+ * Reset game state to initial (for restart)
255
+ */
256
+ resetGame(): void;
257
+ /**
258
+ * Save start player for rejoin flow
259
+ */
260
+ recordStartPlayer(player: PlayerLabel): void;
261
+ /**
262
+ * Get move to undo from history
263
+ */
264
+ getLastMove(): TurnEntry | null;
265
+ /**
266
+ * Dispatch APPROVE action with automatic target state resolution
267
+ * Multiple valid transitions exist - use state context to determine target
268
+ */
269
+ dispatchApprove(): void;
270
+ /**
271
+ * Dispatch REJECT action with automatic target state resolution
272
+ * Multiple valid transitions exist - use resumeTurn to determine who continues
273
+ */
274
+ dispatchReject(): void;
275
+ /**
276
+ * Dispatch START action with automatic target state resolution
277
+ * Determines who plays first based on starter parameter
278
+ */
279
+ dispatchStart(firstPlayer: PlayerLabel): void;
280
+ /**
281
+ * Dispatch SYNC_COMPLETE with automatic target state resolution
282
+ * Based on who should have the turn after sync
283
+ */
284
+ dispatchSyncComplete(nextPlayer: PlayerLabel): void;
285
+ /**
286
+ * Set the game plugin for rule validation and win checking
287
+ * @param plugin Implementation of IGamePlugin
288
+ */
289
+ setGamePlugin(plugin: IGamePlugin): void;
290
+ /**
291
+ * Get current game plugin
292
+ */
293
+ getGamePlugin(): IGamePlugin;
294
+ /**
295
+ * Validate a move using the game plugin
296
+ * Called by move handler to check if move is legal
297
+ * @param move The move data to validate
298
+ * @returns Validation result with reason if invalid
299
+ */
300
+ validateMove(move: unknown): ValidationResult;
301
+ /**
302
+ * Check if game has ended (someone won)
303
+ * Called by move handler after move is applied
304
+ * @returns Winner (local/remote) or null if game continues
305
+ */
306
+ checkWin(): PlayerLabel | null;
307
+ /**
308
+ * Cleanup when game ends (for plugin to reset internal state)
309
+ */
310
+ cleanupGame(): void;
311
+ /**
312
+ * Build game state for plugin
313
+ * @private
314
+ */
315
+ private buildGameState;
316
+ }
317
+
318
+ interface ISessionActions {
319
+ ready(): void;
320
+ start(): void;
321
+ move(data: unknown): void;
322
+ undo(): void;
323
+ restart(): void;
324
+ approve(): void;
325
+ reject(): void;
326
+ rejoin(sid: string): void;
327
+ }
328
+ declare class LocalActionsAPI implements ISessionActions {
329
+ private bus;
330
+ constructor(bus: CommandBus);
331
+ ready(): void;
332
+ start(): void;
333
+ move(data: unknown): void;
334
+ undo(): void;
335
+ restart(): void;
336
+ approve(): void;
337
+ reject(): void;
338
+ rejoin(sid: string): void;
339
+ }
340
+
341
+ /**
342
+ * Create a new game session with state management and networking
343
+ * @param sid Session ID for rejoining (optional)
344
+ * @param networkClient Custom network client (optional, creates default if not provided)
345
+ * @returns Session manager with bus, state, observer, net, and send method
346
+ *
347
+ * @example
348
+ * const session = createSession();
349
+ * // UI automatically updates when state changes - no manual observer calls needed!
350
+ * session.observer.subscribe(myUIObserver);
351
+ * session.bus.emit('READY', undefined, 'local');
352
+ * await session.net.connect(remotePeerId);
353
+ */
354
+ declare const createSession: (networkClient: NetworkClient, sid?: string) => {
355
+ bus: CommandBus;
356
+ state: State;
357
+ observer: GameStateObserver;
358
+ net: NetClient;
359
+ actions: LocalActionsAPI;
360
+ send: (message: SessionMessage) => void;
361
+ };
362
+
363
+ export { DefaultGamePlugin, type GameEvent, type GameState, GameStateObserver, type GameStateSnapshot, type IGameObserver, type IGamePlugin, type ISessionActions, type IStateObserver, StateObserverManager, UINotificationAdapter, type ValidationResult, buildGameStateSnapshot, createSession };