p2p-lockstep-kit-session 0.1.2 → 0.1.3

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,362 @@
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 lastNotificationTime;
157
+ private notificationThrottleMs;
158
+ constructor(stateRef: State, uiObserver: GameStateObserver);
159
+ onStateChanged(): void;
160
+ onHistoryChanged(): void;
161
+ onGameReset(): void;
162
+ emitEvent(event: Omit<GameEvent, 'timestamp'>): void;
163
+ }
164
+
165
+ type TurnEntry = {
166
+ turn: number;
167
+ player: 'local' | 'remote';
168
+ move?: any;
169
+ };
170
+ type PlayerLabel = 'local' | 'remote';
171
+ declare class State {
172
+ private local;
173
+ private remote;
174
+ private readonly localId;
175
+ private remoteId;
176
+ private readonly history;
177
+ private pendingAction;
178
+ private pendingUndoCount;
179
+ private resumeTurn;
180
+ private lastStart;
181
+ private gamePlugin;
182
+ private stateObserverManager;
183
+ constructor(id: string | null, remoteId: string | null);
184
+ /**
185
+ * Register an internal observer (like plugin pattern)
186
+ * Use this to connect State mutations to UI updates
187
+ */
188
+ subscribeStateObserver(observer: IStateObserver): void;
189
+ getId(): string | null;
190
+ getremoteId(): string | null;
191
+ setremoteId(id: string): void;
192
+ getState(player: PlayerLabel): SessionState;
193
+ getTurnCount(): number;
194
+ getHistory(): TurnEntry[];
195
+ replaceHistory(entries: TurnEntry[]): void;
196
+ clearHistory(): void;
197
+ pushHistory(entry: TurnEntry): void;
198
+ popHistory(): TurnEntry | null;
199
+ canAction(player: PlayerLabel, action: SessionEvent): boolean;
200
+ /**
201
+ * Dispatch an action and automatically determine target state if unique
202
+ * Only use explicit 'to' parameter for ambiguous transitions (APPROVE, REJECT, etc.)
203
+ *
204
+ * For most actions (READY, MOVE, START, etc.), there's only one valid transition,
205
+ * so we automatically find and apply it.
206
+ */
207
+ dispatch(player: PlayerLabel, action: SessionEvent, to?: SessionState): void;
208
+ setPendingAction(action: 'undo' | 'restart' | null): void;
209
+ getPendingAction(): "undo" | "restart" | null;
210
+ setPendingUndoCount(count: 1 | 2 | null): void;
211
+ getPendingUndoCount(): 1 | 2 | null;
212
+ setLastStart(player: PlayerLabel | null): void;
213
+ getLastStart(): PlayerLabel | null;
214
+ setResumeTurn(player: PlayerLabel | null): void;
215
+ getResumeTurn(): PlayerLabel | null;
216
+ private getPlayerFsm;
217
+ /**
218
+ * Save game state snapshot for undo/restart operations
219
+ */
220
+ private gameSnapshot;
221
+ saveGameSnapshot(snapshot: unknown): void;
222
+ getGameSnapshot(): unknown;
223
+ clearGameSnapshot(): void;
224
+ /**
225
+ * Check if there's a pending action (undo/restart)
226
+ */
227
+ hasPendingAction(): boolean;
228
+ /**
229
+ * Clear all pending states (called after approval/rejection)
230
+ */
231
+ clearPendingStates(): void;
232
+ /**
233
+ * Initialize undo request with undo count and current turn holder
234
+ */
235
+ initializeUndoRequest(undoCount: 1 | 2, resumeTurn: PlayerLabel): void;
236
+ /**
237
+ * Initialize restart request with resume turn
238
+ */
239
+ initializeRestartRequest(resumeTurn: PlayerLabel): void;
240
+ /**
241
+ * Check if pending action is undo
242
+ */
243
+ isPendingUndo(): boolean;
244
+ /**
245
+ * Check if pending action is restart
246
+ */
247
+ isPendingRestart(): boolean;
248
+ /**
249
+ * Apply undo by popping history N times
250
+ */
251
+ applyUndo(count?: 1 | 2): void;
252
+ /**
253
+ * Reset game state to initial (for restart)
254
+ */
255
+ resetGame(): void;
256
+ /**
257
+ * Save start player for rejoin flow
258
+ */
259
+ recordStartPlayer(player: PlayerLabel): void;
260
+ /**
261
+ * Get move to undo from history
262
+ */
263
+ getLastMove(): TurnEntry | null;
264
+ /**
265
+ * Dispatch APPROVE action with automatic target state resolution
266
+ * Multiple valid transitions exist - use state context to determine target
267
+ */
268
+ dispatchApprove(): void;
269
+ /**
270
+ * Dispatch REJECT action with automatic target state resolution
271
+ * Multiple valid transitions exist - use resumeTurn to determine who continues
272
+ */
273
+ dispatchReject(): void;
274
+ /**
275
+ * Dispatch START action with automatic target state resolution
276
+ * Determines who plays first based on starter parameter
277
+ */
278
+ dispatchStart(firstPlayer: PlayerLabel): void;
279
+ /**
280
+ * Dispatch SYNC_COMPLETE with automatic target state resolution
281
+ * Based on who should have the turn after sync
282
+ */
283
+ dispatchSyncComplete(nextPlayer: PlayerLabel): void;
284
+ /**
285
+ * Set the game plugin for rule validation and win checking
286
+ * @param plugin Implementation of IGamePlugin
287
+ */
288
+ setGamePlugin(plugin: IGamePlugin): void;
289
+ /**
290
+ * Get current game plugin
291
+ */
292
+ getGamePlugin(): IGamePlugin;
293
+ /**
294
+ * Validate a move using the game plugin
295
+ * Called by move handler to check if move is legal
296
+ * @param move The move data to validate
297
+ * @returns Validation result with reason if invalid
298
+ */
299
+ validateMove(move: unknown): ValidationResult;
300
+ /**
301
+ * Check if game has ended (someone won)
302
+ * Called by move handler after move is applied
303
+ * @returns Winner (local/remote) or null if game continues
304
+ */
305
+ checkWin(): PlayerLabel | null;
306
+ /**
307
+ * Cleanup when game ends (for plugin to reset internal state)
308
+ */
309
+ cleanupGame(): void;
310
+ /**
311
+ * Build game state for plugin
312
+ * @private
313
+ */
314
+ private buildGameState;
315
+ }
316
+
317
+ interface ISessionActions {
318
+ ready(): void;
319
+ start(): void;
320
+ move(data: unknown): void;
321
+ undo(): void;
322
+ restart(): void;
323
+ approve(): void;
324
+ reject(): void;
325
+ rejoin(sid: string): void;
326
+ }
327
+ declare class LocalActionsAPI implements ISessionActions {
328
+ private bus;
329
+ constructor(bus: CommandBus);
330
+ ready(): void;
331
+ start(): void;
332
+ move(data: unknown): void;
333
+ undo(): void;
334
+ restart(): void;
335
+ approve(): void;
336
+ reject(): void;
337
+ rejoin(sid: string): void;
338
+ }
339
+
340
+ /**
341
+ * Create a new game session with state management and networking
342
+ * @param sid Session ID for rejoining (optional)
343
+ * @param networkClient Custom network client (optional, creates default if not provided)
344
+ * @returns Session manager with bus, state, observer, net, and send method
345
+ *
346
+ * @example
347
+ * const session = createSession();
348
+ * // UI automatically updates when state changes - no manual observer calls needed!
349
+ * session.observer.subscribe(myUIObserver);
350
+ * session.bus.emit('READY', undefined, 'local');
351
+ * await session.net.connect(remotePeerId);
352
+ */
353
+ declare const createSession: (networkClient: NetworkClient, sid?: string) => {
354
+ bus: CommandBus;
355
+ state: State;
356
+ observer: GameStateObserver;
357
+ net: NetClient;
358
+ actions: LocalActionsAPI;
359
+ send: (message: SessionMessage) => void;
360
+ };
361
+
362
+ export { DefaultGamePlugin, type GameEvent, type GameState, GameStateObserver, type GameStateSnapshot, type IGameObserver, type IGamePlugin, type ISessionActions, type IStateObserver, StateObserverManager, UINotificationAdapter, type ValidationResult, buildGameStateSnapshot, createSession };