p2p-lockstep-kit-session 0.1.6 → 0.1.7

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.
@@ -215,6 +215,10 @@ declare class State {
215
215
  setResumeTurn(player: PlayerLabel | null): void;
216
216
  getResumeTurn(): PlayerLabel | null;
217
217
  private getPlayerFsm;
218
+ private notifyStateChanged;
219
+ private notifyHistoryChanged;
220
+ private notifyGameReset;
221
+ private dispatchPair;
218
222
  /**
219
223
  * Save game state snapshot for undo/restart operations
220
224
  */
@@ -246,9 +246,9 @@ function buildGameStateSnapshot(state, connected = false) {
246
246
  }
247
247
  var UINotificationAdapter = class {
248
248
  constructor(stateRef, uiObserver, getConnected = () => false) {
249
- __publicField(this, "stateRef", stateRef);
250
- __publicField(this, "uiObserver", uiObserver);
251
- __publicField(this, "getConnected", getConnected);
249
+ this.stateRef = stateRef;
250
+ this.uiObserver = uiObserver;
251
+ this.getConnected = getConnected;
252
252
  __publicField(this, "lastNotificationTime", 0);
253
253
  __publicField(this, "notificationThrottleMs", 0);
254
254
  }
@@ -338,11 +338,11 @@ var State = class {
338
338
  }
339
339
  clearHistory() {
340
340
  this.history.splice(0, this.history.length);
341
- this.stateObserverManager.notifyHistoryChanged();
341
+ this.notifyHistoryChanged();
342
342
  }
343
343
  pushHistory(entry) {
344
344
  this.history.push(entry);
345
- this.stateObserverManager.notifyHistoryChanged();
345
+ this.notifyHistoryChanged();
346
346
  }
347
347
  popHistory() {
348
348
  return this.history.pop() ?? null;
@@ -359,7 +359,7 @@ var State = class {
359
359
  */
360
360
  dispatch(player, action, to) {
361
361
  this.getPlayerFsm(player).dispatch(action, to);
362
- this.stateObserverManager.notifyStateChanged();
362
+ this.notifyStateChanged();
363
363
  }
364
364
  setPendingAction(action) {
365
365
  this.pendingAction = action;
@@ -388,6 +388,20 @@ var State = class {
388
388
  getPlayerFsm(player) {
389
389
  return player === "local" ? this.local : this.remote;
390
390
  }
391
+ notifyStateChanged() {
392
+ this.stateObserverManager.notifyStateChanged();
393
+ }
394
+ notifyHistoryChanged() {
395
+ this.stateObserverManager.notifyHistoryChanged();
396
+ }
397
+ notifyGameReset() {
398
+ this.stateObserverManager.notifyGameReset();
399
+ }
400
+ dispatchPair(localAction, localTo, remoteAction, remoteTo) {
401
+ this.local.dispatch(localAction, localTo);
402
+ this.remote.dispatch(remoteAction, remoteTo);
403
+ this.notifyStateChanged();
404
+ }
391
405
  saveGameSnapshot(snapshot) {
392
406
  this.gameSnapshot = snapshot;
393
407
  }
@@ -410,6 +424,7 @@ var State = class {
410
424
  this.pendingAction = null;
411
425
  this.pendingUndoCount = null;
412
426
  this.resumeTurn = null;
427
+ this.notifyStateChanged();
413
428
  }
414
429
  /**
415
430
  * Initialize undo request with undo count and current turn holder
@@ -454,8 +469,11 @@ var State = class {
454
469
  this.local = new SessionFsm("idle");
455
470
  this.remote = new SessionFsm("idle");
456
471
  this.lastStart = null;
472
+ this.pendingAction = null;
473
+ this.pendingUndoCount = null;
457
474
  this.resumeTurn = null;
458
- this.stateObserverManager.notifyGameReset();
475
+ this.notifyGameReset();
476
+ this.notifyStateChanged();
459
477
  }
460
478
  /**
461
479
  * Save start player for rejoin flow
@@ -477,11 +495,9 @@ var State = class {
477
495
  dispatchApprove() {
478
496
  const localState = this.local.getState();
479
497
  if (localState === "waiting_approval") {
480
- this.local.dispatch("APPROVE", "turn");
481
- this.remote.dispatch("APPROVE", "turn");
498
+ this.dispatchPair("APPROVE", "turn", "APPROVE", "remote_turn");
482
499
  } else if (localState === "approving") {
483
- this.local.dispatch("APPROVE", "remote_turn");
484
- this.remote.dispatch("APPROVE", "remote_turn");
500
+ this.dispatchPair("APPROVE", "remote_turn", "APPROVE", "turn");
485
501
  }
486
502
  }
487
503
  /**
@@ -491,9 +507,9 @@ var State = class {
491
507
  dispatchReject() {
492
508
  const localState = this.local.getState();
493
509
  if (localState === "waiting_approval" || localState === "approving") {
494
- const targetState = this.resumeTurn === "local" ? "turn" : "remote_turn";
495
- this.local.dispatch("REJECT", targetState);
496
- this.remote.dispatch("REJECT", targetState);
510
+ const localTarget = this.resumeTurn === "local" ? "turn" : "remote_turn";
511
+ const remoteTarget = this.resumeTurn === "local" ? "remote_turn" : "turn";
512
+ this.dispatchPair("REJECT", localTarget, "REJECT", remoteTarget);
497
513
  }
498
514
  }
499
515
  /**
@@ -516,14 +532,12 @@ var State = class {
516
532
  * Based on who should have the turn after sync
517
533
  */
518
534
  dispatchSyncComplete(nextPlayer) {
535
+ this.resumeTurn = nextPlayer;
519
536
  if (nextPlayer === "local") {
520
- this.local.dispatch("SYNC_COMPLETE", "turn");
521
- this.remote.dispatch("SYNC_COMPLETE", "remote_turn");
537
+ this.dispatchPair("SYNC_COMPLETE", "turn", "SYNC_COMPLETE", "remote_turn");
522
538
  } else {
523
- this.local.dispatch("SYNC_COMPLETE", "remote_turn");
524
- this.remote.dispatch("SYNC_COMPLETE", "turn");
539
+ this.dispatchPair("SYNC_COMPLETE", "remote_turn", "SYNC_COMPLETE", "turn");
525
540
  }
526
- this.resumeTurn = nextPlayer;
527
541
  }
528
542
  // ===== Game Plugin Integration (Proxy Pattern) =====
529
543
  /**
@@ -634,8 +648,8 @@ var parseSessionMessage = (data) => {
634
648
  // session/net.ts
635
649
  var NetClient = class {
636
650
  constructor(client, bus, peerId) {
637
- __publicField(this, "client", client);
638
- __publicField(this, "bus", bus);
651
+ this.client = client;
652
+ this.bus = bus;
639
653
  __publicField(this, "localPeerId");
640
654
  __publicField(this, "remotePeerId");
641
655
  __publicField(this, "isConnected", false);
@@ -1196,7 +1210,7 @@ var registerHandlers = (bus) => {
1196
1210
  // session/actions.ts
1197
1211
  var LocalActionsAPI = class {
1198
1212
  constructor(bus) {
1199
- __publicField(this, "bus", bus);
1213
+ this.bus = bus;
1200
1214
  }
1201
1215
  ready() {
1202
1216
  this.bus.emit("READY");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../session/commandBus.ts","../../session/state/fsm.ts","../../session/observer/index.ts","../../session/state/state.ts","../../utils/logger.ts","../../utils/serialization/json.ts","../../utils/protocol/session.ts","../../session/net.ts","../../session/context.ts","../../session/handlers/ready.ts","../../session/handlers/start.ts","../../session/handlers/move.ts","../../session/handlers/request.ts","../../session/handlers/sync.ts","../../session/handlers/undo.ts","../../session/handlers/restart.ts","../../session/handlers/offLine.ts","../../session/handlers/busRegister.ts","../../session/actions.ts","../../session/index.ts"],"sourcesContent":["import { SessionMessage, SessionMessageType } from '../utils';\n\nexport type CommandOrigin = 'local' | 'remote';\nexport type BusMessageType = SessionMessageType | 'OFFLINE' | 'ONLINE' | 'GAME_OVER';\nexport type BusMessage = Omit<SessionMessage, 'type'> & { type: BusMessageType };\nexport type CommandListener = (message: BusMessage) => Promise<void> | void;\n\ntype HandlerMap = Partial<Record<BusMessageType, CommandListener>>;\n\nexport class CommandBus {\n private handlers: HandlerMap = {};\n private processingQueue: Promise<void> = Promise.resolve();\n\n public emit(type: BusMessageType, payload?: unknown, from: CommandOrigin = 'local'): void {\n this.dispatch({ type, payload, from });\n }\n\n public register(type: BusMessageType, handler: CommandListener): void {\n this.handlers[type] = handler;\n }\n\n public dispatch(message: BusMessage): void {\n this.processingQueue = this.processingQueue.then(async () => {\n const handler = this.handlers[message.type];\n if (handler) {\n try {\n await handler(message);\n } catch (err) {\n console.error(`[CommandBus] Error in ${message.type}:`, err);\n }\n }\n });\n }\n}\n\n","export type SessionState =\n | 'idle'\n | 'ready'\n | 'could_start'\n | 'turn'\n | 'remote_turn'\n | 'approving'\n | 'waiting_approval'\n | 'syncing'\n | 'offline';\n\nexport type SessionEvent =\n | 'REMOTE_READY'\n | 'READY'\n | 'START'\n | 'REMOTE_START'\n | 'MOVE'\n | 'REMOTE_MOVE'\n | 'UNDO'\n | 'REMOTE_UNDO'\n | 'RESTART'\n | 'REMOTE_RESTART'\n | 'APPROVE'\n | 'REJECT'\n | 'GAME_OVER'\n | 'REJOIN'\n | 'SYNC'\n | 'SYNC_COMPLETE'\n | 'OFFLINE'\n | 'ONLINE';\n\nexport type Transition = {\n from: SessionState;\n event: SessionEvent;\n to: SessionState;\n};\n\nconst transitions: Transition[] = [\n // Lobby readiness\n { from: 'idle', event: 'READY', to: 'ready' },\n { from: 'ready', event: 'READY', to: 'idle' },\n { from: 'idle', event: 'REMOTE_READY', to: 'could_start' },\n { from: 'could_start', event: 'REMOTE_READY', to: 'idle' },\n { from: 'ready', event: 'REJECT', to: 'idle' },\n { from: 'could_start', event: 'REJECT', to: 'idle' },\n\n // Match start / turn assignment\n { from: 'ready', event: 'REMOTE_START', to: 'turn' },\n { from: 'ready', event: 'REMOTE_START', to: 'remote_turn' },\n { from: 'could_start', event: 'START', to: 'turn' },\n { from: 'could_start', event: 'START', to: 'remote_turn' },\n\n // Turn swapping after moves\n { from: 'turn', event: 'MOVE', to: 'remote_turn' },\n { from: 'remote_turn', event: 'REMOTE_MOVE', to: 'turn' },\n { from: 'turn', event: 'REJECT', to: 'turn' },\n { from: 'remote_turn', event: 'REJECT', to: 'remote_turn' },\n\n // Requests initiated by local player (undo/restart)\n { from: 'turn', event: 'UNDO', to: 'waiting_approval' },\n { from: 'remote_turn', event: 'UNDO', to: 'waiting_approval' },\n { from: 'turn', event: 'RESTART', to: 'waiting_approval' },\n { from: 'remote_turn', event: 'RESTART', to: 'waiting_approval' },\n\n // Requests coming from remote (we need to approve)\n { from: 'turn', event: 'REMOTE_UNDO', to: 'approving' },\n { from: 'remote_turn', event: 'REMOTE_UNDO', to: 'approving' },\n { from: 'turn', event: 'REMOTE_RESTART', to: 'approving' },\n { from: 'remote_turn', event: 'REMOTE_RESTART', to: 'approving' },\n\n // Approval outcomes when we were waiting\n { from: 'waiting_approval', event: 'APPROVE', to: 'turn' },\n { from: 'waiting_approval', event: 'REJECT', to: 'turn' },\n { from: 'waiting_approval', event: 'REJECT', to: 'remote_turn' },\n\n // Approval outcomes when we were confirming\n { from: 'approving', event: 'APPROVE', to: 'remote_turn' },\n { from: 'approving', event: 'REJECT', to: 'remote_turn' },\n { from: 'approving', event: 'REJECT', to: 'turn' },\n\n // Game end resets back to lobby idle\n { from: 'turn', event: 'GAME_OVER', to: 'idle' },\n { from: 'remote_turn', event: 'GAME_OVER', to: 'idle' },\n\n // Rejoin/sync flows\n { from: 'turn', event: 'SYNC', to: 'syncing' },\n { from: 'remote_turn', event: 'SYNC', to: 'syncing' },\n { from: 'waiting_approval', event: 'SYNC', to: 'syncing' },\n { from: 'approving', event: 'SYNC', to: 'syncing' },\n { from: 'idle', event: 'SYNC', to: 'syncing' },\n { from: 'ready', event: 'SYNC', to: 'syncing' },\n { from: 'could_start', event: 'SYNC', to: 'syncing' },\n { from: 'syncing', event: 'SYNC_COMPLETE', to: 'turn' },\n { from: 'syncing', event: 'SYNC_COMPLETE', to: 'remote_turn' },\n\n // Connection state\n { from: 'idle', event: 'OFFLINE', to: 'offline' },\n { from: 'ready', event: 'OFFLINE', to: 'offline' },\n { from: 'could_start', event: 'OFFLINE', to: 'offline' },\n { from: 'turn', event: 'OFFLINE', to: 'offline' },\n { from: 'remote_turn', event: 'OFFLINE', to: 'offline' },\n { from: 'waiting_approval', event: 'OFFLINE', to: 'offline' },\n { from: 'approving', event: 'OFFLINE', to: 'offline' },\n { from: 'syncing', event: 'OFFLINE', to: 'offline' },\n { from: 'offline', event: 'ONLINE', to: 'syncing' },\n];\n// only receive 'to' from state. never receive from handlers.\nconst nextState = (\n state: SessionState,\n event: SessionEvent,\n to?: SessionState, // next turn or pending action\n): SessionState => {\n if (to) {\n if (\n !!transitions.find(\n (t) => t.from === state && t.event === event && t.to === to,\n )\n ) {\n return to;\n } else {\n return state;\n }\n } else {\n const hit = transitions.find((t) => t.from === state && t.event === event);\n return hit ? hit.to : state;\n }\n};\n\nconst hasNextState = (\n state: SessionState,\n action: SessionEvent,\n to?: SessionState,\n): boolean => {\n if (to) {\n return !!transitions.find(\n (t) => t.from === state && t.event === action && t.to === to,\n );\n }\n return !!transitions.find((t) => t.from === state && t.event === action);\n};\n\nexport class SessionFsm {\n private state: SessionState;\n\n constructor(state: SessionState = 'idle') {\n this.state = state;\n }\n\n public getState(): SessionState {\n return this.state;\n }\n\n public hasNextState(event: SessionEvent, to?: SessionState): boolean {\n return hasNextState(this.state, event, to);\n }\n\n public dispatch(action: SessionEvent, to?: SessionState) {\n this.state = nextState(this.state, action, to);\n }\n}\n","import type { PlayerLabel, TurnEntry, State } from '../state/state';\nimport type { SessionState } from '../state/fsm';\n\nexport interface GameStateSnapshot {\n localState: SessionState;\n remoteState: SessionState;\n turn: number;\n history: TurnEntry[];\n lastStart: PlayerLabel | null;\n pendingAction: 'undo' | 'restart' | null;\n connected: boolean;\n}\n\nexport interface GameEvent {\n type: 'READY' | 'START' | 'MOVE' | 'GAME_OVER' | 'UNDO' | 'RESTART' | 'OFFLINE' | 'ONLINE' | 'SYNC' | 'ERROR';\n payload?: any;\n from?: 'local' | 'remote';\n timestamp?: number;\n}\n\nexport interface IGameObserver {\n onStateChange(snapshot: GameStateSnapshot): void;\n onGameEvent(event: GameEvent): void;\n onConnectionChange?(connected: boolean): void;\n onError?(error: { message: string; context?: any }): void;\n}\n\nexport interface IStateObserver {\n onStateChanged?(): void;\n onHistoryChanged?(): void;\n onGameReset?(): void;\n}\n\nexport interface IGamePlugin {\n validateMove(move: unknown, gameState: GameState): ValidationResult;\n checkWin(gameState: GameState, history: TurnEntry[]): PlayerLabel | null;\n initialize?(): void;\n cleanup?(): void;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n reason?: string;\n}\n\nexport interface GameState {\n history: TurnEntry[];\n localState: 'turn' | 'remote_turn' | string;\n remoteState: 'turn' | 'remote_turn' | string;\n turn: number;\n lastStart: PlayerLabel | null;\n}\n\nexport class DefaultGamePlugin implements IGamePlugin {\n validateMove(): ValidationResult {\n return { valid: true };\n }\n checkWin(): PlayerLabel | null {\n return null;\n }\n}\n\nexport class StateObserverManager {\n private observers: Set<IStateObserver> = new Set();\n\n subscribe(observer: IStateObserver): void {\n this.observers.add(observer);\n }\n\n unsubscribe(observer: IStateObserver): void {\n this.observers.delete(observer);\n }\n\n notifyStateChanged(): void {\n for (const observer of this.observers) {\n try {\n observer.onStateChanged?.();\n } catch (err) {\n console.error('[StateObserver]', err);\n }\n }\n }\n\n notifyHistoryChanged(): void {\n for (const observer of this.observers) {\n try {\n observer.onHistoryChanged?.();\n } catch (err) {\n console.error('[StateObserver]', err);\n }\n }\n }\n\n notifyGameReset(): void {\n for (const observer of this.observers) {\n try {\n observer.onGameReset?.();\n } catch (err) {\n console.error('[StateObserver]', err);\n }\n }\n }\n}\n\nexport class GameStateObserver {\n private observers: Set<IGameObserver> = new Set();\n private currentSnapshot: GameStateSnapshot | null = null;\n\n subscribe(observer: IGameObserver): () => void {\n this.observers.add(observer);\n return () => {\n this.observers.delete(observer);\n };\n }\n\n unsubscribe(observer: IGameObserver): void {\n this.observers.delete(observer);\n }\n\n notifyStateChange(snapshot: GameStateSnapshot): void {\n this.currentSnapshot = snapshot;\n for (const observer of this.observers) {\n try {\n observer.onStateChange(snapshot);\n } catch (err) {\n console.error('[GameStateObserver]', err);\n }\n }\n }\n\n notifyGameEvent(event: GameEvent): void {\n event.timestamp = Date.now();\n for (const observer of this.observers) {\n try {\n observer.onGameEvent(event);\n } catch (err) {\n console.error('[GameStateObserver]', err);\n }\n }\n }\n\n notifyConnectionChange(connected: boolean): void {\n for (const observer of this.observers) {\n try {\n observer.onConnectionChange?.(connected);\n } catch (err) {\n console.error('[GameStateObserver]', err);\n }\n }\n }\n\n notifyError(error: { message: string; context?: any }): void {\n for (const observer of this.observers) {\n try {\n observer.onError?.(error);\n } catch (err) {\n console.error('[GameStateObserver]', err);\n }\n }\n }\n\n getSnapshot(): GameStateSnapshot | null {\n return this.currentSnapshot;\n }\n\n getObserverCount(): number {\n return this.observers.size;\n }\n}\n\nexport function buildGameStateSnapshot(state: State, connected: boolean = false): GameStateSnapshot {\n return {\n localState: state.getState('local'),\n remoteState: state.getState('remote'),\n turn: state.getTurnCount(),\n history: state.getHistory(),\n lastStart: state.getLastStart(),\n pendingAction: state.getPendingAction(),\n connected,\n };\n}\n\nexport class UINotificationAdapter implements IStateObserver {\n private lastNotificationTime = 0;\n private notificationThrottleMs = 0;\n\n constructor(\n private stateRef: State,\n private uiObserver: GameStateObserver,\n private getConnected: () => boolean = () => false,\n ) {}\n\n onStateChanged(): void {\n const now = Date.now();\n if (this.lastNotificationTime + this.notificationThrottleMs > now) return;\n this.lastNotificationTime = now;\n\n const snapshot = buildGameStateSnapshot(this.stateRef, this.getConnected());\n this.uiObserver.notifyStateChange(snapshot);\n }\n\n onHistoryChanged(): void {}\n\n onGameReset(): void {}\n\n emitEvent(event: Omit<GameEvent, 'timestamp'>): void {\n this.uiObserver.notifyGameEvent(event as GameEvent);\n }\n}\n","import { SessionEvent, SessionFsm, SessionState } from './fsm';\nimport type {\n IGamePlugin,\n GameState,\n ValidationResult,\n IStateObserver,\n} from '../observer';\nimport { DefaultGamePlugin, StateObserverManager } from '../observer';\n\nexport type TurnEntry = {\n turn: number;\n player: 'local' | 'remote';\n move?: any;\n};\n\nexport type PlayerLabel = 'local' | 'remote';\n\nexport class State {\n // will update map when multi-players (>=3)\n private local = new SessionFsm('idle');\n private remote = new SessionFsm('idle');\n // for compare remote is same people or not\n private readonly localId: string | null = null;\n private remoteId: string | null = null;\n // store all actions\n private readonly history: TurnEntry[] = [];\n // pending some state\n private pendingAction: 'undo' | 'restart' | null = null;\n private pendingUndoCount: 1 | 2 | null = null;\n private resumeTurn: PlayerLabel | null = null;\n private lastStart: PlayerLabel | null = null;\n\n // Game plugin for rule validation and win checking\n private gamePlugin: IGamePlugin = new DefaultGamePlugin();\n\n // Internal state observer for UI notifications\n private stateObserverManager = new StateObserverManager();\n\n constructor(id: string | null, remoteId: string | null) {\n if (id) {\n this.localId = id;\n }\n if (remoteId) {\n this.remoteId = remoteId;\n }\n }\n\n /**\n * Register an internal observer (like plugin pattern)\n * Use this to connect State mutations to UI updates\n */\n public subscribeStateObserver(observer: IStateObserver): void {\n this.stateObserverManager.subscribe(observer);\n }\n\n // ...existing code...\n\n public getId(): string | null {\n return this.localId;\n }\n\n public getremoteId(): string | null {\n return this.remoteId;\n }\n\n public setremoteId(id: string) {\n this.remoteId = id;\n }\n\n public getState(player: PlayerLabel): SessionState {\n return this.getPlayerFsm(player).getState();\n }\n\n public getTurnCount(): number {\n return this.history.length + 1;\n }\n\n public getHistory(): TurnEntry[] {\n return this.history.slice();\n }\n\n public replaceHistory(entries: TurnEntry[]): void {\n this.clearHistory();\n entries.forEach((entry) => {\n this.pushHistory({\n turn: entry.turn,\n player: entry.player,\n move: entry.move,\n });\n });\n }\n\n public clearHistory(): void {\n this.history.splice(0, this.history.length);\n this.stateObserverManager.notifyHistoryChanged();\n }\n\n public pushHistory(entry: TurnEntry): void {\n this.history.push(entry);\n this.stateObserverManager.notifyHistoryChanged();\n }\n\n public popHistory(): TurnEntry | null {\n return this.history.pop() ?? null;\n }\n\n public canAction(\n player: PlayerLabel,\n action: SessionEvent,\n ): boolean {\n return this.getPlayerFsm(player).hasNextState(action);\n }\n\n /**\n * Dispatch an action and automatically determine target state if unique\n * Only use explicit 'to' parameter for ambiguous transitions (APPROVE, REJECT, etc.)\n *\n * For most actions (READY, MOVE, START, etc.), there's only one valid transition,\n * so we automatically find and apply it.\n */\n public dispatch(\n player: PlayerLabel,\n action: SessionEvent,\n to?: SessionState,\n ): void {\n this.getPlayerFsm(player).dispatch(action, to);\n // Notify all state observers after state change\n this.stateObserverManager.notifyStateChanged();\n }\n\n public setPendingAction(action: 'undo' | 'restart' | null) {\n this.pendingAction = action;\n }\n\n public getPendingAction() {\n return this.pendingAction;\n }\n\n public setPendingUndoCount(count: 1 | 2 | null) {\n this.pendingUndoCount = count;\n }\n\n public getPendingUndoCount(): 1 | 2 | null {\n return this.pendingUndoCount;\n }\n\n public setLastStart(player: PlayerLabel | null) {\n this.lastStart = player;\n }\n\n public getLastStart(): PlayerLabel | null {\n return this.lastStart;\n }\n\n public setResumeTurn(player: PlayerLabel | null) {\n this.resumeTurn = player;\n }\n\n public getResumeTurn(): PlayerLabel | null {\n return this.resumeTurn;\n }\n\n private getPlayerFsm(player: PlayerLabel): SessionFsm {\n return player === 'local' ? this.local : this.remote;\n }\n\n // ===== Helper Methods for Undo/Restart Request Handling =====\n\n /**\n * Save game state snapshot for undo/restart operations\n */\n private gameSnapshot: unknown = null;\n\n public saveGameSnapshot(snapshot: unknown): void {\n this.gameSnapshot = snapshot;\n }\n\n public getGameSnapshot(): unknown {\n return this.gameSnapshot;\n }\n\n public clearGameSnapshot(): void {\n this.gameSnapshot = null;\n }\n\n /**\n * Check if there's a pending action (undo/restart)\n */\n public hasPendingAction(): boolean {\n return this.pendingAction !== null;\n }\n\n /**\n * Clear all pending states (called after approval/rejection)\n */\n public clearPendingStates(): void {\n this.pendingAction = null;\n this.pendingUndoCount = null;\n this.resumeTurn = null;\n }\n\n /**\n * Initialize undo request with undo count and current turn holder\n */\n public initializeUndoRequest(undoCount: 1 | 2, resumeTurn: PlayerLabel): void {\n this.pendingAction = 'undo';\n this.pendingUndoCount = undoCount;\n this.resumeTurn = resumeTurn;\n }\n\n /**\n * Initialize restart request with resume turn\n */\n public initializeRestartRequest(resumeTurn: PlayerLabel): void {\n this.pendingAction = 'restart';\n this.resumeTurn = resumeTurn;\n }\n\n /**\n * Check if pending action is undo\n */\n public isPendingUndo(): boolean {\n return this.pendingAction === 'undo';\n }\n\n /**\n * Check if pending action is restart\n */\n public isPendingRestart(): boolean {\n return this.pendingAction === 'restart';\n }\n\n /**\n * Apply undo by popping history N times\n */\n public applyUndo(count: 1 | 2 = 1): void {\n for (let i = 0; i < count; i++) {\n this.popHistory();\n }\n }\n\n /**\n * Reset game state to initial (for restart)\n */\n public resetGame(): void {\n this.clearHistory();\n this.local = new SessionFsm('idle');\n this.remote = new SessionFsm('idle');\n this.lastStart = null;\n this.resumeTurn = null;\n this.stateObserverManager.notifyGameReset();\n }\n\n /**\n * Save start player for rejoin flow\n */\n public recordStartPlayer(player: PlayerLabel): void {\n this.lastStart = player;\n }\n\n /**\n * Get move to undo from history\n */\n public getLastMove(): TurnEntry | null {\n return this.history.length > 0 ? this.history[this.history.length - 1] : null;\n }\n\n // ===== Specialized FSM Dispatch Methods =====\n\n /**\n * Dispatch APPROVE action with automatic target state resolution\n * Multiple valid transitions exist - use state context to determine target\n */\n public dispatchApprove(): void {\n const localState = this.local.getState();\n if (localState === 'waiting_approval') {\n // Local was waiting, always go back to turn\n this.local.dispatch('APPROVE', 'turn');\n this.remote.dispatch('APPROVE', 'turn');\n } else if (localState === 'approving') {\n // Local was confirming, peer takes turn\n this.local.dispatch('APPROVE', 'remote_turn');\n this.remote.dispatch('APPROVE', 'remote_turn');\n }\n }\n\n /**\n * Dispatch REJECT action with automatic target state resolution\n * Multiple valid transitions exist - use resumeTurn to determine who continues\n */\n public dispatchReject(): void {\n const localState = this.local.getState();\n\n if (localState === 'waiting_approval' || localState === 'approving') {\n // resumeTurn tells us who should have turn after rejection\n const targetState = this.resumeTurn === 'local' ? 'turn' : 'remote_turn';\n this.local.dispatch('REJECT', targetState);\n this.remote.dispatch('REJECT', targetState);\n }\n }\n\n /**\n * Dispatch START action with automatic target state resolution\n * Determines who plays first based on starter parameter\n */\n public dispatchStart(firstPlayer: PlayerLabel): void {\n if (firstPlayer === 'local') {\n this.local.dispatch('START', 'turn');\n this.remote.dispatch('START', 'remote_turn');\n this.lastStart = 'local';\n } else {\n this.local.dispatch('START', 'remote_turn');\n this.remote.dispatch('START', 'turn');\n this.lastStart = 'remote';\n }\n }\n\n /**\n * Dispatch SYNC_COMPLETE with automatic target state resolution\n * Based on who should have the turn after sync\n */\n public dispatchSyncComplete(nextPlayer: PlayerLabel): void {\n if (nextPlayer === 'local') {\n this.local.dispatch('SYNC_COMPLETE', 'turn');\n this.remote.dispatch('SYNC_COMPLETE', 'remote_turn');\n } else {\n this.local.dispatch('SYNC_COMPLETE', 'remote_turn');\n this.remote.dispatch('SYNC_COMPLETE', 'turn');\n }\n this.resumeTurn = nextPlayer;\n }\n\n // ===== Game Plugin Integration (Proxy Pattern) =====\n\n /**\n * Set the game plugin for rule validation and win checking\n * @param plugin Implementation of IGamePlugin\n */\n public setGamePlugin(plugin: IGamePlugin): void {\n this.gamePlugin = plugin;\n if (plugin.initialize) {\n plugin.initialize();\n }\n }\n\n /**\n * Get current game plugin\n */\n public getGamePlugin(): IGamePlugin {\n return this.gamePlugin;\n }\n\n /**\n * Validate a move using the game plugin\n * Called by move handler to check if move is legal\n * @param move The move data to validate\n * @returns Validation result with reason if invalid\n */\n public validateMove(move: unknown): ValidationResult {\n const gameState = this.buildGameState();\n return this.gamePlugin.validateMove(move, gameState);\n }\n\n /**\n * Check if game has ended (someone won)\n * Called by move handler after move is applied\n * @returns Winner (local/remote) or null if game continues\n */\n public checkWin(): PlayerLabel | null {\n const gameState = this.buildGameState();\n return this.gamePlugin.checkWin(gameState, this.getHistory());\n }\n\n /**\n * Cleanup when game ends (for plugin to reset internal state)\n */\n public cleanupGame(): void {\n if (this.gamePlugin.cleanup) {\n this.gamePlugin.cleanup();\n }\n }\n\n /**\n * Build game state for plugin\n * @private\n */\n private buildGameState(): GameState {\n return {\n history: this.getHistory(),\n localState: this.getState('local'),\n remoteState: this.getState('remote'),\n turn: this.getTurnCount(),\n lastStart: this.getLastStart(),\n };\n }\n}\n","export type Logger = {\n debug: (message: string, meta?: unknown) => void;\n info: (message: string, meta?: unknown) => void;\n warn: (message: string, meta?: unknown) => void;\n error: (message: string, meta?: unknown) => void;\n};\n\nconst logWith =\n (level: 'debug' | 'info' | 'warn' | 'error') =>\n (message: string, meta?: unknown) => {\n if (meta !== undefined) {\n // eslint-disable-next-line no-console\n console[level](message, meta);\n return;\n }\n // eslint-disable-next-line no-console\n console[level](message);\n };\n\nexport const consoleLogger: Logger = {\n debug: logWith('debug'),\n info: logWith('info'),\n warn: logWith('warn'),\n error: logWith('error'),\n};\n","export type Serialized = string;\n\nexport const encode = (value: unknown): Serialized => JSON.stringify(value);\n\nexport const decode = <T>(raw: Serialized): T => {\n if (typeof raw !== 'string') {\n throw new TypeError('decode expects a serialized string');\n }\n return JSON.parse(raw) as T;\n};\n\nexport const decodeSafe = <T>(\n raw: unknown,\n): { ok: true; value: T } | { ok: false; error: unknown } => {\n if (typeof raw !== 'string') {\n return {\n ok: false,\n error: new TypeError('decodeSafe expects a serialized string'),\n };\n }\n\n try {\n return { ok: true, value: JSON.parse(raw) as T };\n } catch (error) {\n return { ok: false, error };\n }\n};\n","import { decodeSafe } from '../serialization';\n\nexport type SessionMessageType =\n | 'READY'\n | 'START'\n | 'MOVE'\n | 'UNDO'\n | 'RESTART'\n | 'APPROVE'\n | 'REJECT'\n | 'REJOIN'\n | 'SYNC_REQUEST'\n | 'SYNC_STATE'\n | 'OFFLINE'\n | 'ONLINE'\n | 'GAME_OVER';\n\nexport type SessionMessage = {\n type: SessionMessageType;\n from?: string;\n seq?: number;\n sid?: string;\n turn?: number;\n stateHash?: string;\n payload?: any;\n};\n\nexport const parseSessionMessage = (\n data: unknown,\n): (Partial<SessionMessage> & { type?: string }) | null => {\n if (typeof data !== 'string') {\n if (!data || typeof data !== 'object') {\n return null;\n }\n return data as Partial<SessionMessage> & { type?: string };\n }\n\n const decoded = decodeSafe<Partial<SessionMessage> & { type?: string }>(\n data,\n );\n if (!decoded.ok || !decoded.value || typeof decoded.value !== 'object') {\n return null;\n }\n\n return decoded.value;\n};\n","import type { NetworkClient } from 'p2p-lockstep-kit-network';\nimport { parseSessionMessage, type SessionMessage } from '../utils';\nimport type { BusMessageType, CommandBus } from './commandBus';\n\n/**\n * Network client wrapper that bridges NetworkClient with CommandBus\n * Handles message encoding/decoding and connection state monitoring\n */\nexport class NetClient {\n private localPeerId: string | null;\n private remotePeerId: string | null;\n private isConnected: boolean = false;\n private connectionChangeListener: (isConnected: boolean) => void = () => {};\n private mediaStateListener: (active: boolean) => void = () => {};\n\n public constructor(\n private readonly client: NetworkClient,\n private readonly bus: CommandBus,\n peerId: string | null,\n ) {\n this.localPeerId = peerId ?? null;\n this.remotePeerId = null;\n\n // Forward incoming messages to the command bus\n this.client.onMessage((data) => {\n const message = parseSessionMessage(data);\n if (!message || typeof message !== 'object' || !message.type) {\n return;\n }\n this.bus.dispatch({\n ...(message as SessionMessage),\n type: message.type as BusMessageType,\n from: 'remote',\n });\n });\n\n // Monitor connection state and emit ONLINE/OFFLINE events\n this.client.onStateChange((state) => {\n const wasConnected = this.isConnected;\n this.isConnected = state === 'connected';\n\n // Notify listeners of connection state change\n this.connectionChangeListener(this.isConnected);\n\n // Only emit ONLINE/OFFLINE once when state changes\n if (this.isConnected && !wasConnected) {\n this.bus.emit('ONLINE', undefined, 'local');\n } else if (!this.isConnected && wasConnected) {\n this.bus.emit('OFFLINE', undefined, 'local');\n }\n });\n\n // Monitor remote media stream availability\n this.client.onRemoteStream((stream) => {\n const active =\n !!stream &&\n stream.getTracks().some((track) => track.readyState === 'live');\n this.mediaStateListener(active);\n });\n }\n\n /**\n * Send a message to the remote peer\n * Drops message if not connected and logs warning\n */\n public send(message: SessionMessage) {\n if (!this.isConnected) {\n console.warn(\n '[NetClient] Cannot send message: not connected',\n message.type,\n );\n return;\n }\n\n const enriched: SessionMessage = {\n ...message,\n from: message.from ?? this.localPeerId ?? '',\n };\n this.client.send(enriched);\n }\n\n /**\n * Update local and remote peer IDs\n */\n public setPeerIds(ids: { local?: string | null; remote?: string | null }) {\n if (ids.local !== undefined) {\n this.localPeerId = ids.local;\n }\n if (ids.remote !== undefined) {\n this.remotePeerId = ids.remote;\n }\n }\n\n /**\n * Get current peer IDs\n */\n public getPeerIds() {\n return { local: this.localPeerId, remote: this.remotePeerId };\n }\n\n /**\n * Check if currently connected to peer\n */\n public getIsConnected(): boolean {\n return this.isConnected;\n }\n\n /**\n * Monitor connection state changes\n * @param handler Called when connection state changes (true=connected, false=disconnected)\n */\n public onConnectionChange(handler: (isConnected: boolean) => void) {\n this.connectionChangeListener = handler;\n // Immediately call with current state if already have a state\n handler(this.isConnected);\n }\n\n /**\n * Monitor remote media stream state changes\n * @param handler Called when remote media becomes available or unavailable\n */\n public onMediaStateChange(handler: (active: boolean) => void) {\n this.mediaStateListener = handler;\n }\n}\n\nexport const createNetClient = (\n client: NetworkClient,\n bus: CommandBus,\n peerId: string | null,\n) => new NetClient(client, bus, peerId);\n","import type { State } from './state/state';\nimport type { CommandBus } from './commandBus';\nimport type { SessionMessage } from '../utils';\nimport { NetClient } from './net';\n\n/**\n * Global session context holder\n * Stores the current session's dependencies for handler access\n * Used by handlers to retrieve state, bus, and network client\n */\nclass SessionContext {\n private readonly state: State;\n private readonly bus: CommandBus;\n private readonly net: NetClient;\n private readonly sid?: string;\n\n constructor(state: State, bus: CommandBus, net: NetClient, sid?: string) {\n this.state = state;\n this.bus = bus;\n this.net = net;\n this.sid = sid;\n }\n\n getState() {\n return this.state;\n }\n\n getBus() {\n return this.bus;\n }\n\n getNet() {\n return this.net;\n }\n\n getSid() {\n return this.sid;\n }\n}\n\nlet instance: SessionContext | null = null;\n\n/**\n * Initialize the global session context\n * Must be called before handlers use context getters\n * @throws Error if context is accessed before initialization\n */\nexport const initializeContext = (\n state: State,\n bus: CommandBus,\n net: NetClient,\n sid?: string,\n) => {\n instance = new SessionContext(state, bus, net, sid);\n};\n\n/**\n * Reset the session context (for testing)\n * @internal Used by tests to clean up between test cases\n */\nexport const resetContext = () => {\n instance = null;\n};\n\n/**\n * Get the current session context\n * @throws Error if context has not been initialized\n * @internal Use getState(), getBus(), etc. instead\n */\nconst requireContext = () => {\n if (!instance) {\n throw new Error(\n '[SessionContext] Not initialized. Call initializeContext() first.',\n );\n }\n return instance;\n};\n\n/**\n * Get the current game state\n * @throws Error if context has not been initialized\n */\nexport const getState = () => requireContext().getState();\n\n/**\n * Get the command bus\n * @throws Error if context has not been initialized\n */\nexport const getBus = () => requireContext().getBus();\n\n/**\n * Get the network client\n * @throws Error if context has not been initialized\n * @internal Prefer using send() for sending messages\n */\nexport const getNet = () => requireContext().getNet();\n\n/**\n * Get the session ID\n * @throws Error if context has not been initialized\n */\nexport const getSid = () => requireContext().getSid();\n\n/**\n * Send a session message to the remote peer\n * @throws Error if context has not been initialized\n */\nexport const send = (message: SessionMessage) =>\n requireContext().getNet().send(message);\n\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getSid, getState, send } from '../context';\nimport type { SessionMessage } from '../../utils';\n\n/**\n * Handle player ready status notification\n *\n * For most cases, there's only one valid FSM transition:\n * - READY: idle → ready (for initiator)\n * - REMOTE_READY: idle → could_start (for peer)\n *\n * The dispatch() method automatically finds the unique transition.\n */\nexport const ready: CommandListener = (command) => {\n const state = getState();\n const bus = getBus();\n const localSid = getSid();\n\n if (command.from === 'local') {\n // Local player initiating READY\n if (!state.canAction('local', 'READY')) {\n console.warn('[Ready] Cannot dispatch READY from current state', {\n state: state.getState('local'),\n });\n return;\n }\n\n state.dispatch('local', 'READY');\n state.dispatch('remote', 'REMOTE_READY');\n\n const message: SessionMessage = {\n type: 'READY',\n sid: localSid,\n };\n send(message);\n return;\n }\n\n // Remote player sent READY message\n const remoteSid = (command as any).sid;\n\n // Validate session ID\n if (!remoteSid || localSid !== remoteSid) {\n console.warn('[Ready] Session ID mismatch', {\n local: localSid,\n remote: remoteSid,\n });\n bus.emit('REJECT', { reason: 'sid-mismatch' }, 'local');\n return;\n }\n\n // Check if transition is valid\n if (!state.canAction('remote', 'READY')) {\n console.warn('[Ready] Cannot dispatch READY for remote peer', {\n state: state.getState('remote'),\n });\n return;\n }\n\n // Execute state transitions\n state.dispatch('remote', 'READY');\n state.dispatch('local', 'REMOTE_READY');\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport type { PlayerLabel } from '../state/state';\n\n/**\n * Determine next starter (turn order rotation)\n * If no previous starter, randomly pick one\n * Otherwise, alternate between local and remote\n */\nconst getNextStarter = (lastStarter: PlayerLabel | null): PlayerLabel => {\n if (!lastStarter) {\n return Math.random() < 0.5 ? 'local' : 'remote';\n }\n return lastStarter === 'local' ? 'remote' : 'local';\n};\n\n/**\n * Handle game start request\n *\n * Determines who plays first and transitions both FSMs accordingly.\n */\nexport const start: CommandListener = (command) => {\n const state = getState();\n\n if (command.from === 'local') {\n // Local player initiating START\n if (\n !state.canAction('local', 'START') ||\n !state.canAction('remote', 'REMOTE_START')\n ) {\n console.warn('[Start] Cannot START from current state', {\n localState: state.getState('local'),\n remoteState: state.getState('remote'),\n });\n return;\n }\n\n const nextStarter = getNextStarter(state.getLastStart());\n const localTarget = nextStarter === 'local' ? 'turn' : 'remote_turn';\n const remoteTarget = nextStarter === 'local' ? 'remote_turn' : 'turn';\n\n state.setLastStart(nextStarter);\n state.dispatch('local', 'START', localTarget);\n state.dispatch('remote', 'REMOTE_START', remoteTarget);\n\n // Send message with starter info (encoded as 'sender'/'receiver')\n send({\n type: 'START',\n payload: { starter: nextStarter === 'local' ? 'sender' : 'receiver' },\n });\n return;\n }\n\n // Remote player sent START message\n const starterInfo = (command as any).payload?.starter;\n\n if (!starterInfo) {\n console.warn('[Start] Invalid START message format', { payload: command });\n return;\n }\n\n // Check if transition is valid\n if (\n !state.canAction('local', 'REMOTE_START') ||\n !state.canAction('remote', 'START')\n ) {\n console.warn('[Start] Cannot START from current state', {\n localState: state.getState('local'),\n remoteState: state.getState('remote'),\n });\n return;\n }\n\n // Decode from the receiver's perspective: the sender is the remote peer.\n const starter = starterInfo === 'sender' ? 'remote' : 'local';\n const localTarget = starter === 'local' ? 'turn' : 'remote_turn';\n const remoteTarget = starter === 'local' ? 'remote_turn' : 'turn';\n\n state.setLastStart(starter);\n state.dispatch('local', 'REMOTE_START', localTarget);\n state.dispatch('remote', 'START', remoteTarget);\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState, send } from '../context';\nimport type { SessionMessage } from '../../utils';\n\n/**\n * Handle player move in game\n *\n * Flow:\n * 1. Validate move using game plugin (rule checking)\n * 2. Dispatch state transitions\n * 3. Record move in history\n * 4. Check win condition using game plugin\n * 5. Send to peer or emit GAME_OVER if won\n *\n * The game plugin is injected via state.setGamePlugin(), allowing\n * different games to provide their own rule validation and win logic.\n */\nexport const move: CommandListener = (command) => {\n const state = getState();\n const bus = getBus();\n const movePayload = command.payload;\n\n if (command.from === 'local') {\n // Local player making a move\n if (!state.canAction('local', 'MOVE')) {\n console.warn('[Move] Cannot MOVE from current state', {\n state: state.getState('local'),\n });\n return;\n }\n\n // ===== PROXY POINT 1: Validate move using game plugin =====\n const validation = state.validateMove(movePayload);\n if (!validation.valid) {\n console.warn('[Move] Move validation failed', {\n reason: validation.reason,\n move: movePayload,\n });\n // Could send REJECT to inform peer, but for now silently return\n return;\n }\n\n // Dispatch state transitions\n state.dispatch('local', 'MOVE');\n state.dispatch('remote', 'REMOTE_MOVE');\n\n // Record move in history\n const turn = state.getTurnCount();\n state.pushHistory({\n turn,\n player: 'local',\n move: movePayload,\n });\n\n // ===== PROXY POINT 2: Check win condition using game plugin =====\n const winner = state.checkWin();\n if (winner) {\n // Game ended - someone won\n console.log('[Move] Game over, winner:', winner);\n\n // Notify bus that game ended (for UI to display winner)\n bus.emit('GAME_OVER', { winner, turn }, 'local');\n\n // Send game over to peer\n send({\n type: 'GAME_OVER',\n payload: { winner, turn },\n });\n\n // Transition both FSMs back to idle (game ready to restart)\n state.dispatch('local', 'GAME_OVER');\n state.dispatch('remote', 'GAME_OVER');\n state.cleanupGame();\n return;\n }\n\n // Send move to peer\n const message: SessionMessage = {\n type: 'MOVE',\n turn,\n payload: movePayload,\n };\n send(message);\n return;\n }\n\n // Remote player made a move\n if (!state.canAction('remote', 'MOVE')) {\n console.warn('[Move] Cannot MOVE for remote player', {\n state: state.getState('remote'),\n });\n return;\n }\n\n // ===== PROXY POINT 1: Validate remote move =====\n const validation = state.validateMove(movePayload);\n if (!validation.valid) {\n console.warn('[Move] Remote move validation failed', {\n reason: validation.reason,\n move: movePayload,\n });\n return;\n }\n\n // Dispatch state transitions\n state.dispatch('remote', 'MOVE');\n state.dispatch('local', 'REMOTE_MOVE');\n\n // Record move in history\n const turn = state.getTurnCount();\n state.pushHistory({\n turn,\n player: 'remote',\n move: movePayload,\n });\n\n // ===== PROXY POINT 2: Check win condition =====\n const winner = state.checkWin();\n if (winner) {\n // Game ended - someone won\n console.log('[Move] Game over, winner:', winner);\n\n // Notify bus that game ended (for UI to display winner)\n bus.emit('GAME_OVER', { winner, turn }, 'local');\n\n // Transition both FSMs back to idle (game ready to restart)\n state.dispatch('local', 'GAME_OVER');\n state.dispatch('remote', 'GAME_OVER');\n state.cleanupGame();\n }\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\n\n/**\n * Handle approval/rejection of pending requests (undo/restart)\n *\n * When local player approves/rejects a pending request:\n * - Apply undo/restart state changes (clear history, pop moves)\n * - Use dispatchApprove/dispatchReject for complex state transitions\n * - Notify peer of decision\n *\n * When remote player approves/rejects local's request:\n * - Similar flow but opposite state transitions\n */\nexport const request: CommandListener = (command) => {\n if (command.type !== 'APPROVE' && command.type !== 'REJECT') {\n return;\n }\n\n const state = getState();\n const action = state.getPendingAction();\n\n // No pending action to respond to\n if (!action) {\n console.warn('[Request] No pending action', { commandType: command.type });\n return;\n }\n\n const payload = command.payload as { action?: string; reason?: string } | undefined;\n\n // Verify payload matches pending action\n if (payload?.action && payload.action !== action) {\n console.warn('[Request] Action mismatch', { pending: action, payload: payload.action });\n return;\n }\n\n if (command.from === 'local') {\n if (command.type === 'APPROVE') {\n // Local player approves the request\n if (!state.canAction('local', 'APPROVE')) {\n console.warn('[Request] Cannot APPROVE from current state');\n return;\n }\n\n // Use special method for complex APPROVE transition\n state.dispatchApprove();\n\n // Apply the approved action (undo or restart)\n if (action === 'undo') {\n state.applyUndo(state.getPendingUndoCount() ?? 1);\n } else if (action === 'restart') {\n state.resetGame();\n }\n\n send({ type: 'APPROVE', payload: { action } });\n state.clearPendingStates();\n return;\n }\n\n // REJECT from local\n if (!state.canAction('local', 'REJECT')) {\n console.warn('[Request] Cannot REJECT from current state');\n return;\n }\n\n // Use special method for complex REJECT transition\n state.dispatchReject();\n\n send({\n type: 'REJECT',\n payload: { action, reason: payload?.reason ?? 'rejected' },\n });\n state.clearPendingStates();\n return;\n }\n\n // Remote player responded to local's request\n if (command.type === 'APPROVE') {\n // Remote player approves\n if (!state.canAction('local', 'APPROVE')) {\n console.warn('[Request] Cannot APPROVE from current state (remote approved)');\n return;\n }\n\n // Use special method for complex APPROVE transition\n state.dispatchApprove();\n\n // Apply the approved action (undo or restart)\n if (action === 'undo') {\n state.applyUndo(state.getPendingUndoCount() ?? 1);\n } else if (action === 'restart') {\n state.resetGame();\n }\n\n state.clearPendingStates();\n return;\n }\n\n // REJECT from remote\n if (!state.canAction('local', 'REJECT')) {\n console.warn('[Request] Cannot REJECT from current state (remote rejected)');\n state.clearPendingStates();\n return;\n }\n\n // Use special method for complex REJECT transition\n state.dispatchReject();\n state.clearPendingStates();\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport type { PlayerLabel } from '../state/state';\n\n/**\n * Handle game state synchronization after disconnect/reconnect\n *\n * SYNC_REQUEST: Initiator sends sync request, responder sends back complete game state\n * SYNC_STATE: Received game state, restore it, and transition both players to correct turn\n *\n * Synced data:\n * - history: All moves in order\n * - lastStart: Who started the last match (for turn rotation)\n * - turn: Current turn holder (to determine resume turn)\n * - resumeTurn: Who should have turn after sync (saved before disconnect)\n *\n * Flow:\n * 1. Local disconnects -> offline handler records resumeTurn\n * 2. Local reconnects -> online handler sends SYNC_REQUEST\n * 3. Remote responds with SYNC_STATE (history, lastStart, turn, resumeTurn)\n * 4. Local receives SYNC_STATE -> restores everything, calls dispatchSyncComplete\n * 5. Both FSMs now in correct 'turn'/'remote_turn' state\n */\nexport const sync: CommandListener = (command) => {\n const state = getState();\n\n if (command.type === 'SYNC_REQUEST') {\n if (command.from === 'local') {\n // Local player initiated sync (after reconnection)\n if (!state.canAction('local', 'SYNC')) {\n console.warn('[Sync] Cannot SYNC from current state');\n return;\n }\n\n // Both transition to syncing state\n state.dispatch('local', 'SYNC', 'syncing');\n state.dispatch('remote', 'SYNC', 'syncing');\n\n // Send request to peer (peer will respond with SYNC_STATE)\n send({ type: 'SYNC_REQUEST', from: '', payload: command.payload });\n return;\n }\n\n // Remote initiated sync - respond with complete game state\n const payload = {\n history: state.getHistory(),\n lastStart: state.getLastStart(),\n turn: state.getState('local') === 'turn' ? 'local' : 'remote',\n resumeTurn: state.getResumeTurn(), // Send back the saved resume turn\n };\n send({ type: 'SYNC_STATE', from: '', payload });\n return;\n }\n\n if (command.type !== 'SYNC_STATE') {\n return;\n }\n\n // Received complete game state from peer\n const payload = (command.payload as {\n history?: Array<{ turn: number; player: 'local' | 'remote'; move?: unknown }>;\n lastStart?: 'local' | 'remote' | null;\n turn?: 'local' | 'remote';\n resumeTurn?: 'local' | 'remote' | null;\n }) || {};\n\n // Restore game history from peer\n if (payload.history && payload.history.length > 0) {\n state.replaceHistory(payload.history);\n } else {\n state.clearHistory();\n }\n\n // Restore match start player (for turn rotation in next match)\n if (payload.lastStart) {\n state.setLastStart(payload.lastStart);\n } else {\n state.setLastStart(null);\n }\n\n // Determine who should have turn after sync\n // Priority: 1) resumeTurn from peer (what they saved on disconnect)\n // 2) turn from payload (whose turn it actually was)\n let nextPlayer: PlayerLabel;\n\n if (payload.resumeTurn) {\n // Use the saved resume turn if available\n nextPlayer = payload.resumeTurn;\n } else if (payload.turn) {\n // Otherwise use current turn from peer\n nextPlayer = payload.turn === 'local' ? 'local' : 'remote';\n } else {\n // Fallback to current state\n nextPlayer = state.getState('local') === 'turn' ? 'local' : 'remote';\n }\n\n console.log('[Sync] Restored game state', {\n historyLength: state.getHistory().length,\n lastStart: state.getLastStart(),\n nextTurnPlayer: nextPlayer,\n });\n\n // Use special method for complex SYNC_COMPLETE transition\n // This sets both local and remote FSM to correct 'turn'/'remote_turn' state\n state.dispatchSyncComplete(nextPlayer);\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\n\n/**\n * Handle undo request from local or remote player\n *\n * Validates undo is legal (enough history, no pending action, valid state)\n * and initiates undo request with peer approval flow.\n *\n * Determines undo count (1 or 2 moves) based on current turn holder.\n * Records pending undo state for approval/rejection handling by request handler.\n */\nexport const undo: CommandListener = (command) => {\n if (command.type !== 'UNDO') {\n return;\n }\n\n const state = getState();\n\n if (command.from === 'local') {\n // Validate no pending action\n if (state.hasPendingAction()) {\n return;\n }\n\n // Validate state transitions\n if (!state.canAction('local', 'UNDO')) {\n console.warn('[Undo] Cannot UNDO from current state');\n return;\n }\n\n // Determine undo count based on whose turn it is\n const localState = state.getState('local');\n const undoCount = localState === 'turn' ? 1 : 2;\n\n // Validate history is long enough\n if (state.getHistory().length < undoCount) {\n console.warn('[Undo] Not enough history to undo', { count: undoCount });\n return;\n }\n\n // Initialize pending undo state\n state.initializeUndoRequest(undoCount as 1 | 2, 'local');\n\n // Transition to approval waiting state\n state.dispatch('local', 'UNDO');\n state.dispatch('remote', 'REMOTE_UNDO');\n\n send({ type: 'UNDO', payload: { count: undoCount } });\n return;\n }\n\n // Remote player requested undo\n if (state.hasPendingAction()) {\n send({ type: 'REJECT', payload: { action: 'undo', reason: 'busy' } });\n return;\n }\n\n // Validate state transitions\n if (!state.canAction('local', 'REMOTE_UNDO')) {\n console.warn('[Undo] Cannot accept remote UNDO request');\n send({ type: 'REJECT', payload: { action: 'undo', reason: 'invalid_state' } });\n return;\n }\n\n // Extract undo count from payload\n const payload = command.payload as { count?: number } | undefined;\n const count = payload?.count === 2 ? 2 : 1;\n\n // Validate count value\n if (payload?.count && payload.count !== 1 && payload.count !== 2) {\n send({ type: 'REJECT', payload: { action: 'undo', reason: 'invalid' } });\n return;\n }\n\n // Validate history is long enough\n if (count === 2 && state.getHistory().length < 2) {\n send({ type: 'REJECT', payload: { action: 'undo', reason: 'no_history' } });\n return;\n }\n\n // Determine who will resume after undo\n const resumePlayer = state.getState('local') === 'turn' ? 'local' : 'remote';\n\n // Initialize pending undo state\n state.initializeUndoRequest(count as 1 | 2, resumePlayer);\n\n // Transition to approval waiting state (remote initiated, local approving)\n state.dispatch('local', 'REMOTE_UNDO');\n state.dispatch('remote', 'UNDO');\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\n\n/**\n * Handle restart request from local or remote player\n *\n * Validates restart is legal (no pending action, valid state)\n * and initiates restart request with peer approval flow.\n *\n * Restart clears all history but keeps player order for next match.\n */\nexport const restart: CommandListener = (command) => {\n if (command.type !== 'RESTART') {\n return;\n }\n\n const state = getState();\n\n if (command.from === 'local') {\n // Validate no pending action\n if (state.hasPendingAction()) {\n return;\n }\n\n // Validate state transitions\n if (!state.canAction('local', 'RESTART')) {\n console.warn('[Restart] Cannot RESTART from current state');\n return;\n }\n\n // Determine who will go first in next match\n const resumePlayer = state.getState('local') === 'turn' ? 'local' : 'remote';\n\n // Initialize pending restart state\n state.initializeRestartRequest(resumePlayer);\n\n // Transition to approval waiting state\n state.dispatch('local', 'RESTART');\n state.dispatch('remote', 'REMOTE_RESTART');\n\n send({ type: 'RESTART' });\n return;\n }\n\n // Remote player requested restart\n if (state.hasPendingAction()) {\n send({ type: 'REJECT', payload: { action: 'restart', reason: 'busy' } });\n return;\n }\n\n // Validate state transitions\n if (!state.canAction('local', 'REMOTE_RESTART')) {\n console.warn('[Restart] Cannot accept remote RESTART request');\n send({ type: 'REJECT', payload: { action: 'restart', reason: 'invalid_state' } });\n return;\n }\n\n // Determine who will go first in next match\n const resumePlayer = state.getState('local') === 'turn' ? 'local' : 'remote';\n\n // Initialize pending restart state\n state.initializeRestartRequest(resumePlayer);\n\n // Transition to approval waiting state (remote initiated, local approving)\n state.dispatch('local', 'REMOTE_RESTART');\n state.dispatch('remote', 'RESTART');\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState } from '../context';\n\n/**\n * Handle connection state changes (OFFLINE/ONLINE)\n *\n * OFFLINE: Record current turn state before disconnecting\n * ONLINE: Trigger sync to restore game state after reconnect\n *\n * Flow:\n * 1. OFFLINE arrives -> save resumeTurn, transition to offline\n * 2. ONLINE arrives -> transition to syncing, emit SYNC_REQUEST\n * 3. sync handler takes over -> restore history and turn assignment\n */\nexport const offline: CommandListener = (command) => {\n if (command.type !== 'OFFLINE' && command.type !== 'ONLINE') {\n return;\n }\n\n const state = getState();\n const bus = getBus();\n\n if (command.type === 'OFFLINE') {\n // Peer disconnected - save current turn state for later recovery\n if (!state.canAction('remote', 'OFFLINE')) {\n console.warn('[Offline] Cannot transition to OFFLINE from current state');\n return;\n }\n\n // Record who had the turn before going offline\n const currentTurn = state.getState('local') === 'turn' ? 'local' : 'remote';\n state.setResumeTurn(currentTurn);\n\n // Transition to offline state\n state.dispatch('remote', 'OFFLINE', 'offline');\n return;\n }\n\n // ONLINE only means \"reconnected\" if the session FSM was already offline.\n // Initial WebRTC connection is handled by the connection observer, not this\n // recovery path.\n if (state.getState('remote') !== 'offline') {\n return;\n }\n\n // ONLINE - peer reconnected\n if (!state.canAction('remote', 'ONLINE')) {\n console.warn('[Offline] Cannot transition to ONLINE from current state');\n return;\n }\n\n // Transition to syncing state\n state.dispatch('remote', 'ONLINE', 'syncing');\n\n // Trigger sync to restore game state\n // sync handler will:\n // 1. Send SYNC_REQUEST\n // 2. Receive SYNC_STATE from peer\n // 3. Restore history and correct turn assignment\n bus.emit('SYNC_REQUEST', undefined, 'local');\n};\n","import { CommandBus } from '../commandBus';\nimport { ready } from './ready';\nimport { start } from './start';\nimport { move } from './move';\nimport { request } from './request';\nimport { sync } from './sync';\nimport { undo } from './undo';\nimport { restart } from './restart';\nimport { offline } from './offLine';\n\nexport const registerHandlers = (bus: CommandBus) => {\n bus.register('READY', ready);\n bus.register('START', start);\n bus.register('MOVE', move);\n bus.register('UNDO', undo);\n bus.register('RESTART', restart);\n bus.register('SYNC_REQUEST', sync);\n bus.register('SYNC_STATE', sync);\n bus.register('OFFLINE', offline);\n bus.register('ONLINE', offline);\n bus.register('APPROVE', request);\n bus.register('REJECT', request);\n};\n\n\n","import type { CommandBus } from './commandBus';\n\nexport interface ISessionActions {\n ready(): void;\n start(): void;\n move(data: unknown): void;\n undo(): void;\n restart(): void;\n approve(): void;\n reject(): void;\n rejoin(sid: string): void;\n}\n\nexport class LocalActionsAPI implements ISessionActions {\n constructor(private bus: CommandBus) {}\n\n ready(): void {\n this.bus.emit('READY');\n }\n\n start(): void {\n this.bus.emit('START');\n }\n\n move(data: unknown): void {\n this.bus.emit('MOVE', data);\n }\n\n undo(): void {\n this.bus.emit('UNDO');\n }\n\n restart(): void {\n this.bus.emit('RESTART');\n }\n\n approve(): void {\n this.bus.emit('APPROVE');\n }\n\n reject(): void {\n this.bus.emit('REJECT');\n }\n\n rejoin(sid: string): void {\n this.bus.emit('REJOIN', { sid });\n }\n}\n\n","import { NetworkClient } from 'p2p-lockstep-kit-network';\nimport { CommandBus } from './commandBus';\nimport { State } from './state/state';\nimport { createNetClient } from './net';\nimport { initializeContext } from './context';\nimport { registerHandlers } from './handlers/busRegister.ts';\nimport {\n buildGameStateSnapshot,\n GameStateObserver,\n UINotificationAdapter,\n} from './observer';\nimport { LocalActionsAPI } from './actions';\n\n/**\n * Create a new game session with state management and networking\n * @param sid Session ID for rejoining (optional)\n * @param networkClient Custom network client (optional, creates default if not provided)\n * @returns Session manager with bus, state, observer, net, and send method\n *\n * @example\n * const session = createSession();\n * // UI automatically updates when state changes - no manual observer calls needed!\n * session.observer.subscribe(myUIObserver);\n * session.bus.emit('READY', undefined, 'local');\n * await session.net.connect(remotePeerId);\n */\nexport const createSession = (networkClient: NetworkClient, sid?: string) => {\n const bus = new CommandBus();\n const state = new State(null, null);\n const observer = new GameStateObserver();\n const net = createNetClient(networkClient, bus, null);\n\n // Connect State mutations to UI updates via adapter (plugin pattern)\n // This is the ONLY place where UI notifications are triggered\n const uiAdapter = new UINotificationAdapter(state, observer, () =>\n net.getIsConnected(),\n );\n state.subscribeStateObserver(uiAdapter);\n\n initializeContext(state, bus, net, sid);\n registerHandlers(bus);\n\n const actions = new LocalActionsAPI(bus);\n\n net.onConnectionChange((isConnected) => {\n observer.notifyConnectionChange(isConnected);\n observer.notifyStateChange(buildGameStateSnapshot(state, isConnected));\n });\n\n return {\n bus,\n state,\n observer,\n net,\n actions,\n send: net.send.bind(net),\n };\n};\n\nexport * from './observer';\nexport type { ISessionActions } from './actions';\n"],"mappings":"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","names":["move","localTarget","remoteTarget","validation","turn","winner","payload","resumePlayer"]}
1
+ {"version":3,"sources":["../../session/commandBus.ts","../../session/state/fsm.ts","../../session/observer/index.ts","../../session/state/state.ts","../../utils/logger.ts","../../utils/serialization/json.ts","../../utils/protocol/session.ts","../../session/net.ts","../../session/context.ts","../../session/handlers/ready.ts","../../session/handlers/start.ts","../../session/handlers/move.ts","../../session/handlers/request.ts","../../session/handlers/sync.ts","../../session/handlers/undo.ts","../../session/handlers/restart.ts","../../session/handlers/offLine.ts","../../session/handlers/busRegister.ts","../../session/actions.ts","../../session/index.ts"],"sourcesContent":["import { SessionMessage, SessionMessageType } from '../utils';\n\nexport type CommandOrigin = 'local' | 'remote';\nexport type BusMessageType = SessionMessageType | 'OFFLINE' | 'ONLINE' | 'GAME_OVER';\nexport type BusMessage = Omit<SessionMessage, 'type'> & { type: BusMessageType };\nexport type CommandListener = (message: BusMessage) => Promise<void> | void;\n\ntype HandlerMap = Partial<Record<BusMessageType, CommandListener>>;\n\nexport class CommandBus {\n private handlers: HandlerMap = {};\n private processingQueue: Promise<void> = Promise.resolve();\n\n public emit(type: BusMessageType, payload?: unknown, from: CommandOrigin = 'local'): void {\n this.dispatch({ type, payload, from });\n }\n\n public register(type: BusMessageType, handler: CommandListener): void {\n this.handlers[type] = handler;\n }\n\n public dispatch(message: BusMessage): void {\n this.processingQueue = this.processingQueue.then(async () => {\n const handler = this.handlers[message.type];\n if (handler) {\n try {\n await handler(message);\n } catch (err) {\n console.error(`[CommandBus] Error in ${message.type}:`, err);\n }\n }\n });\n }\n}\n\n","export type SessionState =\n | 'idle'\n | 'ready'\n | 'could_start'\n | 'turn'\n | 'remote_turn'\n | 'approving'\n | 'waiting_approval'\n | 'syncing'\n | 'offline';\n\nexport type SessionEvent =\n | 'REMOTE_READY'\n | 'READY'\n | 'START'\n | 'REMOTE_START'\n | 'MOVE'\n | 'REMOTE_MOVE'\n | 'UNDO'\n | 'REMOTE_UNDO'\n | 'RESTART'\n | 'REMOTE_RESTART'\n | 'APPROVE'\n | 'REJECT'\n | 'GAME_OVER'\n | 'REJOIN'\n | 'SYNC'\n | 'SYNC_COMPLETE'\n | 'OFFLINE'\n | 'ONLINE';\n\nexport type Transition = {\n from: SessionState;\n event: SessionEvent;\n to: SessionState;\n};\n\nconst transitions: Transition[] = [\n // Lobby readiness\n { from: 'idle', event: 'READY', to: 'ready' },\n { from: 'ready', event: 'READY', to: 'idle' },\n { from: 'idle', event: 'REMOTE_READY', to: 'could_start' },\n { from: 'could_start', event: 'REMOTE_READY', to: 'idle' },\n { from: 'ready', event: 'REJECT', to: 'idle' },\n { from: 'could_start', event: 'REJECT', to: 'idle' },\n\n // Match start / turn assignment\n { from: 'ready', event: 'REMOTE_START', to: 'turn' },\n { from: 'ready', event: 'REMOTE_START', to: 'remote_turn' },\n { from: 'could_start', event: 'START', to: 'turn' },\n { from: 'could_start', event: 'START', to: 'remote_turn' },\n\n // Turn swapping after moves\n { from: 'turn', event: 'MOVE', to: 'remote_turn' },\n { from: 'remote_turn', event: 'REMOTE_MOVE', to: 'turn' },\n { from: 'turn', event: 'REJECT', to: 'turn' },\n { from: 'remote_turn', event: 'REJECT', to: 'remote_turn' },\n\n // Requests initiated by local player (undo/restart)\n { from: 'turn', event: 'UNDO', to: 'waiting_approval' },\n { from: 'remote_turn', event: 'UNDO', to: 'waiting_approval' },\n { from: 'turn', event: 'RESTART', to: 'waiting_approval' },\n { from: 'remote_turn', event: 'RESTART', to: 'waiting_approval' },\n\n // Requests coming from remote (we need to approve)\n { from: 'turn', event: 'REMOTE_UNDO', to: 'approving' },\n { from: 'remote_turn', event: 'REMOTE_UNDO', to: 'approving' },\n { from: 'turn', event: 'REMOTE_RESTART', to: 'approving' },\n { from: 'remote_turn', event: 'REMOTE_RESTART', to: 'approving' },\n\n // Approval outcomes when we were waiting\n { from: 'waiting_approval', event: 'APPROVE', to: 'turn' },\n { from: 'waiting_approval', event: 'REJECT', to: 'turn' },\n { from: 'waiting_approval', event: 'REJECT', to: 'remote_turn' },\n\n // Approval outcomes when we were confirming\n { from: 'approving', event: 'APPROVE', to: 'remote_turn' },\n { from: 'approving', event: 'REJECT', to: 'remote_turn' },\n { from: 'approving', event: 'REJECT', to: 'turn' },\n\n // Game end resets back to lobby idle\n { from: 'turn', event: 'GAME_OVER', to: 'idle' },\n { from: 'remote_turn', event: 'GAME_OVER', to: 'idle' },\n\n // Rejoin/sync flows\n { from: 'turn', event: 'SYNC', to: 'syncing' },\n { from: 'remote_turn', event: 'SYNC', to: 'syncing' },\n { from: 'waiting_approval', event: 'SYNC', to: 'syncing' },\n { from: 'approving', event: 'SYNC', to: 'syncing' },\n { from: 'idle', event: 'SYNC', to: 'syncing' },\n { from: 'ready', event: 'SYNC', to: 'syncing' },\n { from: 'could_start', event: 'SYNC', to: 'syncing' },\n { from: 'syncing', event: 'SYNC_COMPLETE', to: 'turn' },\n { from: 'syncing', event: 'SYNC_COMPLETE', to: 'remote_turn' },\n\n // Connection state\n { from: 'idle', event: 'OFFLINE', to: 'offline' },\n { from: 'ready', event: 'OFFLINE', to: 'offline' },\n { from: 'could_start', event: 'OFFLINE', to: 'offline' },\n { from: 'turn', event: 'OFFLINE', to: 'offline' },\n { from: 'remote_turn', event: 'OFFLINE', to: 'offline' },\n { from: 'waiting_approval', event: 'OFFLINE', to: 'offline' },\n { from: 'approving', event: 'OFFLINE', to: 'offline' },\n { from: 'syncing', event: 'OFFLINE', to: 'offline' },\n { from: 'offline', event: 'ONLINE', to: 'syncing' },\n];\n// only receive 'to' from state. never receive from handlers.\nconst nextState = (\n state: SessionState,\n event: SessionEvent,\n to?: SessionState, // next turn or pending action\n): SessionState => {\n if (to) {\n if (\n !!transitions.find(\n (t) => t.from === state && t.event === event && t.to === to,\n )\n ) {\n return to;\n } else {\n return state;\n }\n } else {\n const hit = transitions.find((t) => t.from === state && t.event === event);\n return hit ? hit.to : state;\n }\n};\n\nconst hasNextState = (\n state: SessionState,\n action: SessionEvent,\n to?: SessionState,\n): boolean => {\n if (to) {\n return !!transitions.find(\n (t) => t.from === state && t.event === action && t.to === to,\n );\n }\n return !!transitions.find((t) => t.from === state && t.event === action);\n};\n\nexport class SessionFsm {\n private state: SessionState;\n\n constructor(state: SessionState = 'idle') {\n this.state = state;\n }\n\n public getState(): SessionState {\n return this.state;\n }\n\n public hasNextState(event: SessionEvent, to?: SessionState): boolean {\n return hasNextState(this.state, event, to);\n }\n\n public dispatch(action: SessionEvent, to?: SessionState) {\n this.state = nextState(this.state, action, to);\n }\n}\n","import type { PlayerLabel, TurnEntry, State } from '../state/state';\nimport type { SessionState } from '../state/fsm';\n\nexport interface GameStateSnapshot {\n localState: SessionState;\n remoteState: SessionState;\n turn: number;\n history: TurnEntry[];\n lastStart: PlayerLabel | null;\n pendingAction: 'undo' | 'restart' | null;\n connected: boolean;\n}\n\nexport interface GameEvent {\n type: 'READY' | 'START' | 'MOVE' | 'GAME_OVER' | 'UNDO' | 'RESTART' | 'OFFLINE' | 'ONLINE' | 'SYNC' | 'ERROR';\n payload?: any;\n from?: 'local' | 'remote';\n timestamp?: number;\n}\n\nexport interface IGameObserver {\n onStateChange(snapshot: GameStateSnapshot): void;\n onGameEvent(event: GameEvent): void;\n onConnectionChange?(connected: boolean): void;\n onError?(error: { message: string; context?: any }): void;\n}\n\nexport interface IStateObserver {\n onStateChanged?(): void;\n onHistoryChanged?(): void;\n onGameReset?(): void;\n}\n\nexport interface IGamePlugin {\n validateMove(move: unknown, gameState: GameState): ValidationResult;\n checkWin(gameState: GameState, history: TurnEntry[]): PlayerLabel | null;\n initialize?(): void;\n cleanup?(): void;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n reason?: string;\n}\n\nexport interface GameState {\n history: TurnEntry[];\n localState: 'turn' | 'remote_turn' | string;\n remoteState: 'turn' | 'remote_turn' | string;\n turn: number;\n lastStart: PlayerLabel | null;\n}\n\nexport class DefaultGamePlugin implements IGamePlugin {\n validateMove(): ValidationResult {\n return { valid: true };\n }\n checkWin(): PlayerLabel | null {\n return null;\n }\n}\n\nexport class StateObserverManager {\n private observers: Set<IStateObserver> = new Set();\n\n subscribe(observer: IStateObserver): void {\n this.observers.add(observer);\n }\n\n unsubscribe(observer: IStateObserver): void {\n this.observers.delete(observer);\n }\n\n notifyStateChanged(): void {\n for (const observer of this.observers) {\n try {\n observer.onStateChanged?.();\n } catch (err) {\n console.error('[StateObserver]', err);\n }\n }\n }\n\n notifyHistoryChanged(): void {\n for (const observer of this.observers) {\n try {\n observer.onHistoryChanged?.();\n } catch (err) {\n console.error('[StateObserver]', err);\n }\n }\n }\n\n notifyGameReset(): void {\n for (const observer of this.observers) {\n try {\n observer.onGameReset?.();\n } catch (err) {\n console.error('[StateObserver]', err);\n }\n }\n }\n}\n\nexport class GameStateObserver {\n private observers: Set<IGameObserver> = new Set();\n private currentSnapshot: GameStateSnapshot | null = null;\n\n subscribe(observer: IGameObserver): () => void {\n this.observers.add(observer);\n return () => {\n this.observers.delete(observer);\n };\n }\n\n unsubscribe(observer: IGameObserver): void {\n this.observers.delete(observer);\n }\n\n notifyStateChange(snapshot: GameStateSnapshot): void {\n this.currentSnapshot = snapshot;\n for (const observer of this.observers) {\n try {\n observer.onStateChange(snapshot);\n } catch (err) {\n console.error('[GameStateObserver]', err);\n }\n }\n }\n\n notifyGameEvent(event: GameEvent): void {\n event.timestamp = Date.now();\n for (const observer of this.observers) {\n try {\n observer.onGameEvent(event);\n } catch (err) {\n console.error('[GameStateObserver]', err);\n }\n }\n }\n\n notifyConnectionChange(connected: boolean): void {\n for (const observer of this.observers) {\n try {\n observer.onConnectionChange?.(connected);\n } catch (err) {\n console.error('[GameStateObserver]', err);\n }\n }\n }\n\n notifyError(error: { message: string; context?: any }): void {\n for (const observer of this.observers) {\n try {\n observer.onError?.(error);\n } catch (err) {\n console.error('[GameStateObserver]', err);\n }\n }\n }\n\n getSnapshot(): GameStateSnapshot | null {\n return this.currentSnapshot;\n }\n\n getObserverCount(): number {\n return this.observers.size;\n }\n}\n\nexport function buildGameStateSnapshot(state: State, connected: boolean = false): GameStateSnapshot {\n return {\n localState: state.getState('local'),\n remoteState: state.getState('remote'),\n turn: state.getTurnCount(),\n history: state.getHistory(),\n lastStart: state.getLastStart(),\n pendingAction: state.getPendingAction(),\n connected,\n };\n}\n\nexport class UINotificationAdapter implements IStateObserver {\n private lastNotificationTime = 0;\n private notificationThrottleMs = 0;\n\n constructor(\n private stateRef: State,\n private uiObserver: GameStateObserver,\n private getConnected: () => boolean = () => false,\n ) {}\n\n onStateChanged(): void {\n const now = Date.now();\n if (this.lastNotificationTime + this.notificationThrottleMs > now) return;\n this.lastNotificationTime = now;\n\n const snapshot = buildGameStateSnapshot(this.stateRef, this.getConnected());\n this.uiObserver.notifyStateChange(snapshot);\n }\n\n onHistoryChanged(): void {}\n\n onGameReset(): void {}\n\n emitEvent(event: Omit<GameEvent, 'timestamp'>): void {\n this.uiObserver.notifyGameEvent(event as GameEvent);\n }\n}\n","import { SessionEvent, SessionFsm, SessionState } from './fsm';\nimport type {\n IGamePlugin,\n GameState,\n ValidationResult,\n IStateObserver,\n} from '../observer';\nimport { DefaultGamePlugin, StateObserverManager } from '../observer';\n\nexport type TurnEntry = {\n turn: number;\n player: 'local' | 'remote';\n move?: any;\n};\n\nexport type PlayerLabel = 'local' | 'remote';\n\nexport class State {\n // will update map when multi-players (>=3)\n private local = new SessionFsm('idle');\n private remote = new SessionFsm('idle');\n // for compare remote is same people or not\n private readonly localId: string | null = null;\n private remoteId: string | null = null;\n // store all actions\n private readonly history: TurnEntry[] = [];\n // pending some state\n private pendingAction: 'undo' | 'restart' | null = null;\n private pendingUndoCount: 1 | 2 | null = null;\n private resumeTurn: PlayerLabel | null = null;\n private lastStart: PlayerLabel | null = null;\n\n // Game plugin for rule validation and win checking\n private gamePlugin: IGamePlugin = new DefaultGamePlugin();\n\n // Internal state observer for UI notifications\n private stateObserverManager = new StateObserverManager();\n\n constructor(id: string | null, remoteId: string | null) {\n if (id) {\n this.localId = id;\n }\n if (remoteId) {\n this.remoteId = remoteId;\n }\n }\n\n /**\n * Register an internal observer (like plugin pattern)\n * Use this to connect State mutations to UI updates\n */\n public subscribeStateObserver(observer: IStateObserver): void {\n this.stateObserverManager.subscribe(observer);\n }\n\n // ...existing code...\n\n public getId(): string | null {\n return this.localId;\n }\n\n public getremoteId(): string | null {\n return this.remoteId;\n }\n\n public setremoteId(id: string) {\n this.remoteId = id;\n }\n\n public getState(player: PlayerLabel): SessionState {\n return this.getPlayerFsm(player).getState();\n }\n\n public getTurnCount(): number {\n return this.history.length + 1;\n }\n\n public getHistory(): TurnEntry[] {\n return this.history.slice();\n }\n\n public replaceHistory(entries: TurnEntry[]): void {\n this.clearHistory();\n entries.forEach((entry) => {\n this.pushHistory({\n turn: entry.turn,\n player: entry.player,\n move: entry.move,\n });\n });\n }\n\n public clearHistory(): void {\n this.history.splice(0, this.history.length);\n this.notifyHistoryChanged();\n }\n\n public pushHistory(entry: TurnEntry): void {\n this.history.push(entry);\n this.notifyHistoryChanged();\n }\n\n public popHistory(): TurnEntry | null {\n return this.history.pop() ?? null;\n }\n\n public canAction(\n player: PlayerLabel,\n action: SessionEvent,\n ): boolean {\n return this.getPlayerFsm(player).hasNextState(action);\n }\n\n /**\n * Dispatch an action and automatically determine target state if unique\n * Only use explicit 'to' parameter for ambiguous transitions (APPROVE, REJECT, etc.)\n *\n * For most actions (READY, MOVE, START, etc.), there's only one valid transition,\n * so we automatically find and apply it.\n */\n public dispatch(\n player: PlayerLabel,\n action: SessionEvent,\n to?: SessionState,\n ): void {\n this.getPlayerFsm(player).dispatch(action, to);\n // Notify all state observers after state change\n this.notifyStateChanged();\n }\n\n public setPendingAction(action: 'undo' | 'restart' | null) {\n this.pendingAction = action;\n }\n\n public getPendingAction() {\n return this.pendingAction;\n }\n\n public setPendingUndoCount(count: 1 | 2 | null) {\n this.pendingUndoCount = count;\n }\n\n public getPendingUndoCount(): 1 | 2 | null {\n return this.pendingUndoCount;\n }\n\n public setLastStart(player: PlayerLabel | null) {\n this.lastStart = player;\n }\n\n public getLastStart(): PlayerLabel | null {\n return this.lastStart;\n }\n\n public setResumeTurn(player: PlayerLabel | null) {\n this.resumeTurn = player;\n }\n\n public getResumeTurn(): PlayerLabel | null {\n return this.resumeTurn;\n }\n\n private getPlayerFsm(player: PlayerLabel): SessionFsm {\n return player === 'local' ? this.local : this.remote;\n }\n\n private notifyStateChanged(): void {\n this.stateObserverManager.notifyStateChanged();\n }\n\n private notifyHistoryChanged(): void {\n this.stateObserverManager.notifyHistoryChanged();\n }\n\n private notifyGameReset(): void {\n this.stateObserverManager.notifyGameReset();\n }\n\n private dispatchPair(\n localAction: SessionEvent,\n localTo: SessionState,\n remoteAction: SessionEvent,\n remoteTo: SessionState,\n ): void {\n this.local.dispatch(localAction, localTo);\n this.remote.dispatch(remoteAction, remoteTo);\n this.notifyStateChanged();\n }\n\n // ===== Helper Methods for Undo/Restart Request Handling =====\n\n /**\n * Save game state snapshot for undo/restart operations\n */\n private gameSnapshot: unknown = null;\n\n public saveGameSnapshot(snapshot: unknown): void {\n this.gameSnapshot = snapshot;\n }\n\n public getGameSnapshot(): unknown {\n return this.gameSnapshot;\n }\n\n public clearGameSnapshot(): void {\n this.gameSnapshot = null;\n }\n\n /**\n * Check if there's a pending action (undo/restart)\n */\n public hasPendingAction(): boolean {\n return this.pendingAction !== null;\n }\n\n /**\n * Clear all pending states (called after approval/rejection)\n */\n public clearPendingStates(): void {\n this.pendingAction = null;\n this.pendingUndoCount = null;\n this.resumeTurn = null;\n this.notifyStateChanged();\n }\n\n /**\n * Initialize undo request with undo count and current turn holder\n */\n public initializeUndoRequest(undoCount: 1 | 2, resumeTurn: PlayerLabel): void {\n this.pendingAction = 'undo';\n this.pendingUndoCount = undoCount;\n this.resumeTurn = resumeTurn;\n }\n\n /**\n * Initialize restart request with resume turn\n */\n public initializeRestartRequest(resumeTurn: PlayerLabel): void {\n this.pendingAction = 'restart';\n this.resumeTurn = resumeTurn;\n }\n\n /**\n * Check if pending action is undo\n */\n public isPendingUndo(): boolean {\n return this.pendingAction === 'undo';\n }\n\n /**\n * Check if pending action is restart\n */\n public isPendingRestart(): boolean {\n return this.pendingAction === 'restart';\n }\n\n /**\n * Apply undo by popping history N times\n */\n public applyUndo(count: 1 | 2 = 1): void {\n for (let i = 0; i < count; i++) {\n this.popHistory();\n }\n }\n\n /**\n * Reset game state to initial (for restart)\n */\n public resetGame(): void {\n this.clearHistory();\n this.local = new SessionFsm('idle');\n this.remote = new SessionFsm('idle');\n this.lastStart = null;\n this.pendingAction = null;\n this.pendingUndoCount = null;\n this.resumeTurn = null;\n this.notifyGameReset();\n this.notifyStateChanged();\n }\n\n /**\n * Save start player for rejoin flow\n */\n public recordStartPlayer(player: PlayerLabel): void {\n this.lastStart = player;\n }\n\n /**\n * Get move to undo from history\n */\n public getLastMove(): TurnEntry | null {\n return this.history.length > 0 ? this.history[this.history.length - 1] : null;\n }\n\n // ===== Specialized FSM Dispatch Methods =====\n\n /**\n * Dispatch APPROVE action with automatic target state resolution\n * Multiple valid transitions exist - use state context to determine target\n */\n public dispatchApprove(): void {\n const localState = this.local.getState();\n if (localState === 'waiting_approval') {\n // Local requested the action; peer approved it.\n this.dispatchPair('APPROVE', 'turn', 'APPROVE', 'remote_turn');\n } else if (localState === 'approving') {\n // Peer requested the action; local approved it.\n this.dispatchPair('APPROVE', 'remote_turn', 'APPROVE', 'turn');\n }\n }\n\n /**\n * Dispatch REJECT action with automatic target state resolution\n * Multiple valid transitions exist - use resumeTurn to determine who continues\n */\n public dispatchReject(): void {\n const localState = this.local.getState();\n\n if (localState === 'waiting_approval' || localState === 'approving') {\n // resumeTurn tells us who should have turn after rejection\n const localTarget = this.resumeTurn === 'local' ? 'turn' : 'remote_turn';\n const remoteTarget = this.resumeTurn === 'local' ? 'remote_turn' : 'turn';\n this.dispatchPair('REJECT', localTarget, 'REJECT', remoteTarget);\n }\n }\n\n /**\n * Dispatch START action with automatic target state resolution\n * Determines who plays first based on starter parameter\n */\n public dispatchStart(firstPlayer: PlayerLabel): void {\n if (firstPlayer === 'local') {\n this.local.dispatch('START', 'turn');\n this.remote.dispatch('START', 'remote_turn');\n this.lastStart = 'local';\n } else {\n this.local.dispatch('START', 'remote_turn');\n this.remote.dispatch('START', 'turn');\n this.lastStart = 'remote';\n }\n }\n\n /**\n * Dispatch SYNC_COMPLETE with automatic target state resolution\n * Based on who should have the turn after sync\n */\n public dispatchSyncComplete(nextPlayer: PlayerLabel): void {\n this.resumeTurn = nextPlayer;\n if (nextPlayer === 'local') {\n this.dispatchPair('SYNC_COMPLETE', 'turn', 'SYNC_COMPLETE', 'remote_turn');\n } else {\n this.dispatchPair('SYNC_COMPLETE', 'remote_turn', 'SYNC_COMPLETE', 'turn');\n }\n }\n\n // ===== Game Plugin Integration (Proxy Pattern) =====\n\n /**\n * Set the game plugin for rule validation and win checking\n * @param plugin Implementation of IGamePlugin\n */\n public setGamePlugin(plugin: IGamePlugin): void {\n this.gamePlugin = plugin;\n if (plugin.initialize) {\n plugin.initialize();\n }\n }\n\n /**\n * Get current game plugin\n */\n public getGamePlugin(): IGamePlugin {\n return this.gamePlugin;\n }\n\n /**\n * Validate a move using the game plugin\n * Called by move handler to check if move is legal\n * @param move The move data to validate\n * @returns Validation result with reason if invalid\n */\n public validateMove(move: unknown): ValidationResult {\n const gameState = this.buildGameState();\n return this.gamePlugin.validateMove(move, gameState);\n }\n\n /**\n * Check if game has ended (someone won)\n * Called by move handler after move is applied\n * @returns Winner (local/remote) or null if game continues\n */\n public checkWin(): PlayerLabel | null {\n const gameState = this.buildGameState();\n return this.gamePlugin.checkWin(gameState, this.getHistory());\n }\n\n /**\n * Cleanup when game ends (for plugin to reset internal state)\n */\n public cleanupGame(): void {\n if (this.gamePlugin.cleanup) {\n this.gamePlugin.cleanup();\n }\n }\n\n /**\n * Build game state for plugin\n * @private\n */\n private buildGameState(): GameState {\n return {\n history: this.getHistory(),\n localState: this.getState('local'),\n remoteState: this.getState('remote'),\n turn: this.getTurnCount(),\n lastStart: this.getLastStart(),\n };\n }\n}\n","export type Logger = {\n debug: (message: string, meta?: unknown) => void;\n info: (message: string, meta?: unknown) => void;\n warn: (message: string, meta?: unknown) => void;\n error: (message: string, meta?: unknown) => void;\n};\n\nconst logWith =\n (level: 'debug' | 'info' | 'warn' | 'error') =>\n (message: string, meta?: unknown) => {\n if (meta !== undefined) {\n // eslint-disable-next-line no-console\n console[level](message, meta);\n return;\n }\n // eslint-disable-next-line no-console\n console[level](message);\n };\n\nexport const consoleLogger: Logger = {\n debug: logWith('debug'),\n info: logWith('info'),\n warn: logWith('warn'),\n error: logWith('error'),\n};\n","export type Serialized = string;\n\nexport const encode = (value: unknown): Serialized => JSON.stringify(value);\n\nexport const decode = <T>(raw: Serialized): T => {\n if (typeof raw !== 'string') {\n throw new TypeError('decode expects a serialized string');\n }\n return JSON.parse(raw) as T;\n};\n\nexport const decodeSafe = <T>(\n raw: unknown,\n): { ok: true; value: T } | { ok: false; error: unknown } => {\n if (typeof raw !== 'string') {\n return {\n ok: false,\n error: new TypeError('decodeSafe expects a serialized string'),\n };\n }\n\n try {\n return { ok: true, value: JSON.parse(raw) as T };\n } catch (error) {\n return { ok: false, error };\n }\n};\n","import { decodeSafe } from '../serialization';\n\nexport type SessionMessageType =\n | 'READY'\n | 'START'\n | 'MOVE'\n | 'UNDO'\n | 'RESTART'\n | 'APPROVE'\n | 'REJECT'\n | 'REJOIN'\n | 'SYNC_REQUEST'\n | 'SYNC_STATE'\n | 'OFFLINE'\n | 'ONLINE'\n | 'GAME_OVER';\n\nexport type SessionMessage = {\n type: SessionMessageType;\n from?: string;\n seq?: number;\n sid?: string;\n turn?: number;\n stateHash?: string;\n payload?: any;\n};\n\nexport const parseSessionMessage = (\n data: unknown,\n): (Partial<SessionMessage> & { type?: string }) | null => {\n if (typeof data !== 'string') {\n if (!data || typeof data !== 'object') {\n return null;\n }\n return data as Partial<SessionMessage> & { type?: string };\n }\n\n const decoded = decodeSafe<Partial<SessionMessage> & { type?: string }>(\n data,\n );\n if (!decoded.ok || !decoded.value || typeof decoded.value !== 'object') {\n return null;\n }\n\n return decoded.value;\n};\n","import type { NetworkClient } from 'p2p-lockstep-kit-network';\nimport { parseSessionMessage, type SessionMessage } from '../utils';\nimport type { BusMessageType, CommandBus } from './commandBus';\n\n/**\n * Network client wrapper that bridges NetworkClient with CommandBus\n * Handles message encoding/decoding and connection state monitoring\n */\nexport class NetClient {\n private localPeerId: string | null;\n private remotePeerId: string | null;\n private isConnected: boolean = false;\n private connectionChangeListener: (isConnected: boolean) => void = () => {};\n private mediaStateListener: (active: boolean) => void = () => {};\n\n public constructor(\n private readonly client: NetworkClient,\n private readonly bus: CommandBus,\n peerId: string | null,\n ) {\n this.localPeerId = peerId ?? null;\n this.remotePeerId = null;\n\n // Forward incoming messages to the command bus\n this.client.onMessage((data) => {\n const message = parseSessionMessage(data);\n if (!message || typeof message !== 'object' || !message.type) {\n return;\n }\n this.bus.dispatch({\n ...(message as SessionMessage),\n type: message.type as BusMessageType,\n from: 'remote',\n });\n });\n\n // Monitor connection state and emit ONLINE/OFFLINE events\n this.client.onStateChange((state) => {\n const wasConnected = this.isConnected;\n this.isConnected = state === 'connected';\n\n // Notify listeners of connection state change\n this.connectionChangeListener(this.isConnected);\n\n // Only emit ONLINE/OFFLINE once when state changes\n if (this.isConnected && !wasConnected) {\n this.bus.emit('ONLINE', undefined, 'local');\n } else if (!this.isConnected && wasConnected) {\n this.bus.emit('OFFLINE', undefined, 'local');\n }\n });\n\n // Monitor remote media stream availability\n this.client.onRemoteStream((stream) => {\n const active =\n !!stream &&\n stream.getTracks().some((track) => track.readyState === 'live');\n this.mediaStateListener(active);\n });\n }\n\n /**\n * Send a message to the remote peer\n * Drops message if not connected and logs warning\n */\n public send(message: SessionMessage) {\n if (!this.isConnected) {\n console.warn(\n '[NetClient] Cannot send message: not connected',\n message.type,\n );\n return;\n }\n\n const enriched: SessionMessage = {\n ...message,\n from: message.from ?? this.localPeerId ?? '',\n };\n this.client.send(enriched);\n }\n\n /**\n * Update local and remote peer IDs\n */\n public setPeerIds(ids: { local?: string | null; remote?: string | null }) {\n if (ids.local !== undefined) {\n this.localPeerId = ids.local;\n }\n if (ids.remote !== undefined) {\n this.remotePeerId = ids.remote;\n }\n }\n\n /**\n * Get current peer IDs\n */\n public getPeerIds() {\n return { local: this.localPeerId, remote: this.remotePeerId };\n }\n\n /**\n * Check if currently connected to peer\n */\n public getIsConnected(): boolean {\n return this.isConnected;\n }\n\n /**\n * Monitor connection state changes\n * @param handler Called when connection state changes (true=connected, false=disconnected)\n */\n public onConnectionChange(handler: (isConnected: boolean) => void) {\n this.connectionChangeListener = handler;\n // Immediately call with current state if already have a state\n handler(this.isConnected);\n }\n\n /**\n * Monitor remote media stream state changes\n * @param handler Called when remote media becomes available or unavailable\n */\n public onMediaStateChange(handler: (active: boolean) => void) {\n this.mediaStateListener = handler;\n }\n}\n\nexport const createNetClient = (\n client: NetworkClient,\n bus: CommandBus,\n peerId: string | null,\n) => new NetClient(client, bus, peerId);\n","import type { State } from './state/state';\nimport type { CommandBus } from './commandBus';\nimport type { SessionMessage } from '../utils';\nimport { NetClient } from './net';\n\n/**\n * Global session context holder\n * Stores the current session's dependencies for handler access\n * Used by handlers to retrieve state, bus, and network client\n */\nclass SessionContext {\n private readonly state: State;\n private readonly bus: CommandBus;\n private readonly net: NetClient;\n private readonly sid?: string;\n\n constructor(state: State, bus: CommandBus, net: NetClient, sid?: string) {\n this.state = state;\n this.bus = bus;\n this.net = net;\n this.sid = sid;\n }\n\n getState() {\n return this.state;\n }\n\n getBus() {\n return this.bus;\n }\n\n getNet() {\n return this.net;\n }\n\n getSid() {\n return this.sid;\n }\n}\n\nlet instance: SessionContext | null = null;\n\n/**\n * Initialize the global session context\n * Must be called before handlers use context getters\n * @throws Error if context is accessed before initialization\n */\nexport const initializeContext = (\n state: State,\n bus: CommandBus,\n net: NetClient,\n sid?: string,\n) => {\n instance = new SessionContext(state, bus, net, sid);\n};\n\n/**\n * Reset the session context (for testing)\n * @internal Used by tests to clean up between test cases\n */\nexport const resetContext = () => {\n instance = null;\n};\n\n/**\n * Get the current session context\n * @throws Error if context has not been initialized\n * @internal Use getState(), getBus(), etc. instead\n */\nconst requireContext = () => {\n if (!instance) {\n throw new Error(\n '[SessionContext] Not initialized. Call initializeContext() first.',\n );\n }\n return instance;\n};\n\n/**\n * Get the current game state\n * @throws Error if context has not been initialized\n */\nexport const getState = () => requireContext().getState();\n\n/**\n * Get the command bus\n * @throws Error if context has not been initialized\n */\nexport const getBus = () => requireContext().getBus();\n\n/**\n * Get the network client\n * @throws Error if context has not been initialized\n * @internal Prefer using send() for sending messages\n */\nexport const getNet = () => requireContext().getNet();\n\n/**\n * Get the session ID\n * @throws Error if context has not been initialized\n */\nexport const getSid = () => requireContext().getSid();\n\n/**\n * Send a session message to the remote peer\n * @throws Error if context has not been initialized\n */\nexport const send = (message: SessionMessage) =>\n requireContext().getNet().send(message);\n\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getSid, getState, send } from '../context';\nimport type { SessionMessage } from '../../utils';\n\n/**\n * Handle player ready status notification\n *\n * For most cases, there's only one valid FSM transition:\n * - READY: idle → ready (for initiator)\n * - REMOTE_READY: idle → could_start (for peer)\n *\n * The dispatch() method automatically finds the unique transition.\n */\nexport const ready: CommandListener = (command) => {\n const state = getState();\n const bus = getBus();\n const localSid = getSid();\n\n if (command.from === 'local') {\n // Local player initiating READY\n if (!state.canAction('local', 'READY')) {\n console.warn('[Ready] Cannot dispatch READY from current state', {\n state: state.getState('local'),\n });\n return;\n }\n\n state.dispatch('local', 'READY');\n state.dispatch('remote', 'REMOTE_READY');\n\n const message: SessionMessage = {\n type: 'READY',\n sid: localSid,\n };\n send(message);\n return;\n }\n\n // Remote player sent READY message\n const remoteSid = (command as any).sid;\n\n // Validate session ID\n if (!remoteSid || localSid !== remoteSid) {\n console.warn('[Ready] Session ID mismatch', {\n local: localSid,\n remote: remoteSid,\n });\n bus.emit('REJECT', { reason: 'sid-mismatch' }, 'local');\n return;\n }\n\n // Check if transition is valid\n if (!state.canAction('remote', 'READY')) {\n console.warn('[Ready] Cannot dispatch READY for remote peer', {\n state: state.getState('remote'),\n });\n return;\n }\n\n // Execute state transitions\n state.dispatch('remote', 'READY');\n state.dispatch('local', 'REMOTE_READY');\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport type { PlayerLabel } from '../state/state';\n\n/**\n * Determine next starter (turn order rotation)\n * If no previous starter, randomly pick one\n * Otherwise, alternate between local and remote\n */\nconst getNextStarter = (lastStarter: PlayerLabel | null): PlayerLabel => {\n if (!lastStarter) {\n return Math.random() < 0.5 ? 'local' : 'remote';\n }\n return lastStarter === 'local' ? 'remote' : 'local';\n};\n\n/**\n * Handle game start request\n *\n * Determines who plays first and transitions both FSMs accordingly.\n */\nexport const start: CommandListener = (command) => {\n const state = getState();\n\n if (command.from === 'local') {\n // Local player initiating START\n if (\n !state.canAction('local', 'START') ||\n !state.canAction('remote', 'REMOTE_START')\n ) {\n console.warn('[Start] Cannot START from current state', {\n localState: state.getState('local'),\n remoteState: state.getState('remote'),\n });\n return;\n }\n\n const nextStarter = getNextStarter(state.getLastStart());\n const localTarget = nextStarter === 'local' ? 'turn' : 'remote_turn';\n const remoteTarget = nextStarter === 'local' ? 'remote_turn' : 'turn';\n\n state.setLastStart(nextStarter);\n state.dispatch('local', 'START', localTarget);\n state.dispatch('remote', 'REMOTE_START', remoteTarget);\n\n // Send message with starter info (encoded as 'sender'/'receiver')\n send({\n type: 'START',\n payload: { starter: nextStarter === 'local' ? 'sender' : 'receiver' },\n });\n return;\n }\n\n // Remote player sent START message\n const starterInfo = (command as any).payload?.starter;\n\n if (!starterInfo) {\n console.warn('[Start] Invalid START message format', { payload: command });\n return;\n }\n\n // Check if transition is valid\n if (\n !state.canAction('local', 'REMOTE_START') ||\n !state.canAction('remote', 'START')\n ) {\n console.warn('[Start] Cannot START from current state', {\n localState: state.getState('local'),\n remoteState: state.getState('remote'),\n });\n return;\n }\n\n // Decode from the receiver's perspective: the sender is the remote peer.\n const starter = starterInfo === 'sender' ? 'remote' : 'local';\n const localTarget = starter === 'local' ? 'turn' : 'remote_turn';\n const remoteTarget = starter === 'local' ? 'remote_turn' : 'turn';\n\n state.setLastStart(starter);\n state.dispatch('local', 'REMOTE_START', localTarget);\n state.dispatch('remote', 'START', remoteTarget);\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState, send } from '../context';\nimport type { SessionMessage } from '../../utils';\n\n/**\n * Handle player move in game\n *\n * Flow:\n * 1. Validate move using game plugin (rule checking)\n * 2. Dispatch state transitions\n * 3. Record move in history\n * 4. Check win condition using game plugin\n * 5. Send to peer or emit GAME_OVER if won\n *\n * The game plugin is injected via state.setGamePlugin(), allowing\n * different games to provide their own rule validation and win logic.\n */\nexport const move: CommandListener = (command) => {\n const state = getState();\n const bus = getBus();\n const movePayload = command.payload;\n\n if (command.from === 'local') {\n // Local player making a move\n if (!state.canAction('local', 'MOVE')) {\n console.warn('[Move] Cannot MOVE from current state', {\n state: state.getState('local'),\n });\n return;\n }\n\n // ===== PROXY POINT 1: Validate move using game plugin =====\n const validation = state.validateMove(movePayload);\n if (!validation.valid) {\n console.warn('[Move] Move validation failed', {\n reason: validation.reason,\n move: movePayload,\n });\n // Could send REJECT to inform peer, but for now silently return\n return;\n }\n\n // Dispatch state transitions\n state.dispatch('local', 'MOVE');\n state.dispatch('remote', 'REMOTE_MOVE');\n\n // Record move in history\n const turn = state.getTurnCount();\n state.pushHistory({\n turn,\n player: 'local',\n move: movePayload,\n });\n\n // ===== PROXY POINT 2: Check win condition using game plugin =====\n const winner = state.checkWin();\n if (winner) {\n // Game ended - someone won\n console.log('[Move] Game over, winner:', winner);\n\n // Notify bus that game ended (for UI to display winner)\n bus.emit('GAME_OVER', { winner, turn }, 'local');\n\n // Send game over to peer\n send({\n type: 'GAME_OVER',\n payload: { winner, turn },\n });\n\n // Transition both FSMs back to idle (game ready to restart)\n state.dispatch('local', 'GAME_OVER');\n state.dispatch('remote', 'GAME_OVER');\n state.cleanupGame();\n return;\n }\n\n // Send move to peer\n const message: SessionMessage = {\n type: 'MOVE',\n turn,\n payload: movePayload,\n };\n send(message);\n return;\n }\n\n // Remote player made a move\n if (!state.canAction('remote', 'MOVE')) {\n console.warn('[Move] Cannot MOVE for remote player', {\n state: state.getState('remote'),\n });\n return;\n }\n\n // ===== PROXY POINT 1: Validate remote move =====\n const validation = state.validateMove(movePayload);\n if (!validation.valid) {\n console.warn('[Move] Remote move validation failed', {\n reason: validation.reason,\n move: movePayload,\n });\n return;\n }\n\n // Dispatch state transitions\n state.dispatch('remote', 'MOVE');\n state.dispatch('local', 'REMOTE_MOVE');\n\n // Record move in history\n const turn = state.getTurnCount();\n state.pushHistory({\n turn,\n player: 'remote',\n move: movePayload,\n });\n\n // ===== PROXY POINT 2: Check win condition =====\n const winner = state.checkWin();\n if (winner) {\n // Game ended - someone won\n console.log('[Move] Game over, winner:', winner);\n\n // Notify bus that game ended (for UI to display winner)\n bus.emit('GAME_OVER', { winner, turn }, 'local');\n\n // Transition both FSMs back to idle (game ready to restart)\n state.dispatch('local', 'GAME_OVER');\n state.dispatch('remote', 'GAME_OVER');\n state.cleanupGame();\n }\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\n\n/**\n * Handle approval/rejection of pending requests (undo/restart)\n *\n * When local player approves/rejects a pending request:\n * - Apply undo/restart state changes (clear history, pop moves)\n * - Use dispatchApprove/dispatchReject for complex state transitions\n * - Notify peer of decision\n *\n * When remote player approves/rejects local's request:\n * - Similar flow but opposite state transitions\n */\nexport const request: CommandListener = (command) => {\n if (command.type !== 'APPROVE' && command.type !== 'REJECT') {\n return;\n }\n\n const state = getState();\n const action = state.getPendingAction();\n\n // No pending action to respond to\n if (!action) {\n console.warn('[Request] No pending action', { commandType: command.type });\n return;\n }\n\n const payload = command.payload as { action?: string; reason?: string } | undefined;\n\n // Verify payload matches pending action\n if (payload?.action && payload.action !== action) {\n console.warn('[Request] Action mismatch', { pending: action, payload: payload.action });\n return;\n }\n\n if (command.from === 'local') {\n if (command.type === 'APPROVE') {\n // Local player approves the request\n if (!state.canAction('local', 'APPROVE')) {\n console.warn('[Request] Cannot APPROVE from current state');\n return;\n }\n\n // Use special method for complex APPROVE transition\n state.dispatchApprove();\n\n // Apply the approved action (undo or restart)\n if (action === 'undo') {\n state.applyUndo(state.getPendingUndoCount() ?? 1);\n } else if (action === 'restart') {\n state.resetGame();\n }\n\n send({ type: 'APPROVE', payload: { action } });\n state.clearPendingStates();\n return;\n }\n\n // REJECT from local\n if (!state.canAction('local', 'REJECT')) {\n console.warn('[Request] Cannot REJECT from current state');\n return;\n }\n\n // Use special method for complex REJECT transition\n state.dispatchReject();\n\n send({\n type: 'REJECT',\n payload: { action, reason: payload?.reason ?? 'rejected' },\n });\n state.clearPendingStates();\n return;\n }\n\n // Remote player responded to local's request\n if (command.type === 'APPROVE') {\n // Remote player approves\n if (!state.canAction('local', 'APPROVE')) {\n console.warn('[Request] Cannot APPROVE from current state (remote approved)');\n return;\n }\n\n // Use special method for complex APPROVE transition\n state.dispatchApprove();\n\n // Apply the approved action (undo or restart)\n if (action === 'undo') {\n state.applyUndo(state.getPendingUndoCount() ?? 1);\n } else if (action === 'restart') {\n state.resetGame();\n }\n\n state.clearPendingStates();\n return;\n }\n\n // REJECT from remote\n if (!state.canAction('local', 'REJECT')) {\n console.warn('[Request] Cannot REJECT from current state (remote rejected)');\n state.clearPendingStates();\n return;\n }\n\n // Use special method for complex REJECT transition\n state.dispatchReject();\n state.clearPendingStates();\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport type { PlayerLabel } from '../state/state';\n\n/**\n * Handle game state synchronization after disconnect/reconnect\n *\n * SYNC_REQUEST: Initiator sends sync request, responder sends back complete game state\n * SYNC_STATE: Received game state, restore it, and transition both players to correct turn\n *\n * Synced data:\n * - history: All moves in order\n * - lastStart: Who started the last match (for turn rotation)\n * - turn: Current turn holder (to determine resume turn)\n * - resumeTurn: Who should have turn after sync (saved before disconnect)\n *\n * Flow:\n * 1. Local disconnects -> offline handler records resumeTurn\n * 2. Local reconnects -> online handler sends SYNC_REQUEST\n * 3. Remote responds with SYNC_STATE (history, lastStart, turn, resumeTurn)\n * 4. Local receives SYNC_STATE -> restores everything, calls dispatchSyncComplete\n * 5. Both FSMs now in correct 'turn'/'remote_turn' state\n */\nexport const sync: CommandListener = (command) => {\n const state = getState();\n\n if (command.type === 'SYNC_REQUEST') {\n if (command.from === 'local') {\n // Local player initiated sync (after reconnection)\n if (!state.canAction('local', 'SYNC')) {\n console.warn('[Sync] Cannot SYNC from current state');\n return;\n }\n\n // Both transition to syncing state\n state.dispatch('local', 'SYNC', 'syncing');\n state.dispatch('remote', 'SYNC', 'syncing');\n\n // Send request to peer (peer will respond with SYNC_STATE)\n send({ type: 'SYNC_REQUEST', from: '', payload: command.payload });\n return;\n }\n\n // Remote initiated sync - respond with complete game state\n const payload = {\n history: state.getHistory(),\n lastStart: state.getLastStart(),\n turn: state.getState('local') === 'turn' ? 'local' : 'remote',\n resumeTurn: state.getResumeTurn(), // Send back the saved resume turn\n };\n send({ type: 'SYNC_STATE', from: '', payload });\n return;\n }\n\n if (command.type !== 'SYNC_STATE') {\n return;\n }\n\n // Received complete game state from peer\n const payload = (command.payload as {\n history?: Array<{ turn: number; player: 'local' | 'remote'; move?: unknown }>;\n lastStart?: 'local' | 'remote' | null;\n turn?: 'local' | 'remote';\n resumeTurn?: 'local' | 'remote' | null;\n }) || {};\n\n // Restore game history from peer\n if (payload.history && payload.history.length > 0) {\n state.replaceHistory(payload.history);\n } else {\n state.clearHistory();\n }\n\n // Restore match start player (for turn rotation in next match)\n if (payload.lastStart) {\n state.setLastStart(payload.lastStart);\n } else {\n state.setLastStart(null);\n }\n\n // Determine who should have turn after sync\n // Priority: 1) resumeTurn from peer (what they saved on disconnect)\n // 2) turn from payload (whose turn it actually was)\n let nextPlayer: PlayerLabel;\n\n if (payload.resumeTurn) {\n // Use the saved resume turn if available\n nextPlayer = payload.resumeTurn;\n } else if (payload.turn) {\n // Otherwise use current turn from peer\n nextPlayer = payload.turn === 'local' ? 'local' : 'remote';\n } else {\n // Fallback to current state\n nextPlayer = state.getState('local') === 'turn' ? 'local' : 'remote';\n }\n\n console.log('[Sync] Restored game state', {\n historyLength: state.getHistory().length,\n lastStart: state.getLastStart(),\n nextTurnPlayer: nextPlayer,\n });\n\n // Use special method for complex SYNC_COMPLETE transition\n // This sets both local and remote FSM to correct 'turn'/'remote_turn' state\n state.dispatchSyncComplete(nextPlayer);\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\n\n/**\n * Handle undo request from local or remote player\n *\n * Validates undo is legal (enough history, no pending action, valid state)\n * and initiates undo request with peer approval flow.\n *\n * Determines undo count (1 or 2 moves) based on current turn holder.\n * Records pending undo state for approval/rejection handling by request handler.\n */\nexport const undo: CommandListener = (command) => {\n if (command.type !== 'UNDO') {\n return;\n }\n\n const state = getState();\n\n if (command.from === 'local') {\n // Validate no pending action\n if (state.hasPendingAction()) {\n return;\n }\n\n // Validate state transitions\n if (!state.canAction('local', 'UNDO')) {\n console.warn('[Undo] Cannot UNDO from current state');\n return;\n }\n\n // Determine undo count based on whose turn it is\n const localState = state.getState('local');\n const undoCount = localState === 'turn' ? 1 : 2;\n\n // Validate history is long enough\n if (state.getHistory().length < undoCount) {\n console.warn('[Undo] Not enough history to undo', { count: undoCount });\n return;\n }\n\n // Initialize pending undo state\n state.initializeUndoRequest(undoCount as 1 | 2, 'local');\n\n // Transition to approval waiting state\n state.dispatch('local', 'UNDO');\n state.dispatch('remote', 'REMOTE_UNDO');\n\n send({ type: 'UNDO', payload: { count: undoCount } });\n return;\n }\n\n // Remote player requested undo\n if (state.hasPendingAction()) {\n send({ type: 'REJECT', payload: { action: 'undo', reason: 'busy' } });\n return;\n }\n\n // Validate state transitions\n if (!state.canAction('local', 'REMOTE_UNDO')) {\n console.warn('[Undo] Cannot accept remote UNDO request');\n send({ type: 'REJECT', payload: { action: 'undo', reason: 'invalid_state' } });\n return;\n }\n\n // Extract undo count from payload\n const payload = command.payload as { count?: number } | undefined;\n const count = payload?.count === 2 ? 2 : 1;\n\n // Validate count value\n if (payload?.count && payload.count !== 1 && payload.count !== 2) {\n send({ type: 'REJECT', payload: { action: 'undo', reason: 'invalid' } });\n return;\n }\n\n // Validate history is long enough\n if (count === 2 && state.getHistory().length < 2) {\n send({ type: 'REJECT', payload: { action: 'undo', reason: 'no_history' } });\n return;\n }\n\n // Determine who will resume after undo\n const resumePlayer = state.getState('local') === 'turn' ? 'local' : 'remote';\n\n // Initialize pending undo state\n state.initializeUndoRequest(count as 1 | 2, resumePlayer);\n\n // Transition to approval waiting state (remote initiated, local approving)\n state.dispatch('local', 'REMOTE_UNDO');\n state.dispatch('remote', 'UNDO');\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\n\n/**\n * Handle restart request from local or remote player\n *\n * Validates restart is legal (no pending action, valid state)\n * and initiates restart request with peer approval flow.\n *\n * Restart clears all history but keeps player order for next match.\n */\nexport const restart: CommandListener = (command) => {\n if (command.type !== 'RESTART') {\n return;\n }\n\n const state = getState();\n\n if (command.from === 'local') {\n // Validate no pending action\n if (state.hasPendingAction()) {\n return;\n }\n\n // Validate state transitions\n if (!state.canAction('local', 'RESTART')) {\n console.warn('[Restart] Cannot RESTART from current state');\n return;\n }\n\n // Determine who will go first in next match\n const resumePlayer = state.getState('local') === 'turn' ? 'local' : 'remote';\n\n // Initialize pending restart state\n state.initializeRestartRequest(resumePlayer);\n\n // Transition to approval waiting state\n state.dispatch('local', 'RESTART');\n state.dispatch('remote', 'REMOTE_RESTART');\n\n send({ type: 'RESTART' });\n return;\n }\n\n // Remote player requested restart\n if (state.hasPendingAction()) {\n send({ type: 'REJECT', payload: { action: 'restart', reason: 'busy' } });\n return;\n }\n\n // Validate state transitions\n if (!state.canAction('local', 'REMOTE_RESTART')) {\n console.warn('[Restart] Cannot accept remote RESTART request');\n send({ type: 'REJECT', payload: { action: 'restart', reason: 'invalid_state' } });\n return;\n }\n\n // Determine who will go first in next match\n const resumePlayer = state.getState('local') === 'turn' ? 'local' : 'remote';\n\n // Initialize pending restart state\n state.initializeRestartRequest(resumePlayer);\n\n // Transition to approval waiting state (remote initiated, local approving)\n state.dispatch('local', 'REMOTE_RESTART');\n state.dispatch('remote', 'RESTART');\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState } from '../context';\n\n/**\n * Handle connection state changes (OFFLINE/ONLINE)\n *\n * OFFLINE: Record current turn state before disconnecting\n * ONLINE: Trigger sync to restore game state after reconnect\n *\n * Flow:\n * 1. OFFLINE arrives -> save resumeTurn, transition to offline\n * 2. ONLINE arrives -> transition to syncing, emit SYNC_REQUEST\n * 3. sync handler takes over -> restore history and turn assignment\n */\nexport const offline: CommandListener = (command) => {\n if (command.type !== 'OFFLINE' && command.type !== 'ONLINE') {\n return;\n }\n\n const state = getState();\n const bus = getBus();\n\n if (command.type === 'OFFLINE') {\n // Peer disconnected - save current turn state for later recovery\n if (!state.canAction('remote', 'OFFLINE')) {\n console.warn('[Offline] Cannot transition to OFFLINE from current state');\n return;\n }\n\n // Record who had the turn before going offline\n const currentTurn = state.getState('local') === 'turn' ? 'local' : 'remote';\n state.setResumeTurn(currentTurn);\n\n // Transition to offline state\n state.dispatch('remote', 'OFFLINE', 'offline');\n return;\n }\n\n // ONLINE only means \"reconnected\" if the session FSM was already offline.\n // Initial WebRTC connection is handled by the connection observer, not this\n // recovery path.\n if (state.getState('remote') !== 'offline') {\n return;\n }\n\n // ONLINE - peer reconnected\n if (!state.canAction('remote', 'ONLINE')) {\n console.warn('[Offline] Cannot transition to ONLINE from current state');\n return;\n }\n\n // Transition to syncing state\n state.dispatch('remote', 'ONLINE', 'syncing');\n\n // Trigger sync to restore game state\n // sync handler will:\n // 1. Send SYNC_REQUEST\n // 2. Receive SYNC_STATE from peer\n // 3. Restore history and correct turn assignment\n bus.emit('SYNC_REQUEST', undefined, 'local');\n};\n","import { CommandBus } from '../commandBus';\nimport { ready } from './ready';\nimport { start } from './start';\nimport { move } from './move';\nimport { request } from './request';\nimport { sync } from './sync';\nimport { undo } from './undo';\nimport { restart } from './restart';\nimport { offline } from './offLine';\n\nexport const registerHandlers = (bus: CommandBus) => {\n bus.register('READY', ready);\n bus.register('START', start);\n bus.register('MOVE', move);\n bus.register('UNDO', undo);\n bus.register('RESTART', restart);\n bus.register('SYNC_REQUEST', sync);\n bus.register('SYNC_STATE', sync);\n bus.register('OFFLINE', offline);\n bus.register('ONLINE', offline);\n bus.register('APPROVE', request);\n bus.register('REJECT', request);\n};\n\n\n","import type { CommandBus } from './commandBus';\n\nexport interface ISessionActions {\n ready(): void;\n start(): void;\n move(data: unknown): void;\n undo(): void;\n restart(): void;\n approve(): void;\n reject(): void;\n rejoin(sid: string): void;\n}\n\nexport class LocalActionsAPI implements ISessionActions {\n constructor(private bus: CommandBus) {}\n\n ready(): void {\n this.bus.emit('READY');\n }\n\n start(): void {\n this.bus.emit('START');\n }\n\n move(data: unknown): void {\n this.bus.emit('MOVE', data);\n }\n\n undo(): void {\n this.bus.emit('UNDO');\n }\n\n restart(): void {\n this.bus.emit('RESTART');\n }\n\n approve(): void {\n this.bus.emit('APPROVE');\n }\n\n reject(): void {\n this.bus.emit('REJECT');\n }\n\n rejoin(sid: string): void {\n this.bus.emit('REJOIN', { sid });\n }\n}\n\n","import { NetworkClient } from 'p2p-lockstep-kit-network';\nimport { CommandBus } from './commandBus';\nimport { State } from './state/state';\nimport { createNetClient } from './net';\nimport { initializeContext } from './context';\nimport { registerHandlers } from './handlers/busRegister.ts';\nimport {\n buildGameStateSnapshot,\n GameStateObserver,\n UINotificationAdapter,\n} from './observer';\nimport { LocalActionsAPI } from './actions';\n\n/**\n * Create a new game session with state management and networking\n * @param sid Session ID for rejoining (optional)\n * @param networkClient Custom network client (optional, creates default if not provided)\n * @returns Session manager with bus, state, observer, net, and send method\n *\n * @example\n * const session = createSession();\n * // UI automatically updates when state changes - no manual observer calls needed!\n * session.observer.subscribe(myUIObserver);\n * session.bus.emit('READY', undefined, 'local');\n * await session.net.connect(remotePeerId);\n */\nexport const createSession = (networkClient: NetworkClient, sid?: string) => {\n const bus = new CommandBus();\n const state = new State(null, null);\n const observer = new GameStateObserver();\n const net = createNetClient(networkClient, bus, null);\n\n // Connect State mutations to UI updates via adapter (plugin pattern)\n // This is the ONLY place where UI notifications are triggered\n const uiAdapter = new UINotificationAdapter(state, observer, () =>\n net.getIsConnected(),\n );\n state.subscribeStateObserver(uiAdapter);\n\n initializeContext(state, bus, net, sid);\n registerHandlers(bus);\n\n const actions = new LocalActionsAPI(bus);\n\n net.onConnectionChange((isConnected) => {\n observer.notifyConnectionChange(isConnected);\n observer.notifyStateChange(buildGameStateSnapshot(state, isConnected));\n });\n\n return {\n bus,\n state,\n observer,\n net,\n actions,\n send: net.send.bind(net),\n };\n};\n\nexport * from './observer';\nexport type { ISessionActions } from './actions';\n"],"mappings":"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","names":["move","localTarget","remoteTarget","validation","turn","winner","payload","resumePlayer"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "p2p-lockstep-kit-session",
3
3
  "description": "Game session and lockstep engine for the p2p lockstep kit.",
4
- "version": "0.1.6",
4
+ "version": "0.1.7",
5
5
  "license": "GPL-3.0-only",
6
6
  "type": "module",
7
7
  "main": "./dist/session/index.js",
@@ -42,6 +42,25 @@ const createConnectedSession = () => {
42
42
  return { client, session };
43
43
  };
44
44
 
45
+ const startGame = async () => {
46
+ const runtime = createConnectedSession();
47
+ runtime.client.inbound({ type: "READY", sid: "demo-room", from: "remote" });
48
+ await waitForBus();
49
+ runtime.session.actions.start();
50
+ await waitForBus();
51
+
52
+ assert.match(
53
+ runtime.session.state.getState("local"),
54
+ /^(turn|remote_turn)$/,
55
+ );
56
+ assert.match(
57
+ runtime.session.state.getState("remote"),
58
+ /^(turn|remote_turn)$/,
59
+ );
60
+
61
+ return runtime;
62
+ };
63
+
45
64
  {
46
65
  const { client, session } = createConnectedSession();
47
66
  await waitForBus();
@@ -81,4 +100,122 @@ const createConnectedSession = () => {
81
100
  assert.equal(session.state.getState("remote"), "ready");
82
101
  }
83
102
 
103
+ {
104
+ const { client, session } = await startGame();
105
+ const snapshots = [];
106
+ session.observer.subscribe({
107
+ onStateChange(snapshot) {
108
+ snapshots.push(snapshot);
109
+ },
110
+ onGameEvent() {},
111
+ });
112
+
113
+ session.actions.restart();
114
+ await waitForBus();
115
+
116
+ assert.equal(session.state.getPendingAction(), "restart");
117
+ assert.equal(session.state.getState("local"), "waiting_approval");
118
+ assert.equal(session.state.getState("remote"), "approving");
119
+
120
+ client.inbound({
121
+ type: "APPROVE",
122
+ payload: { action: "restart" },
123
+ from: "remote",
124
+ });
125
+ await waitForBus();
126
+
127
+ assert.equal(session.state.getPendingAction(), null);
128
+ assert.equal(session.state.getState("local"), "idle");
129
+ assert.equal(session.state.getState("remote"), "idle");
130
+ assert.equal(
131
+ snapshots.some(
132
+ (snapshot) =>
133
+ snapshot.localState === "idle" &&
134
+ snapshot.remoteState === "idle" &&
135
+ snapshot.pendingAction === "restart",
136
+ ),
137
+ false,
138
+ );
139
+ }
140
+
141
+ {
142
+ const { client, session } = await startGame();
143
+ const beforeRestart = {
144
+ local: session.state.getState("local"),
145
+ remote: session.state.getState("remote"),
146
+ };
147
+
148
+ session.actions.restart();
149
+ await waitForBus();
150
+
151
+ client.inbound({
152
+ type: "REJECT",
153
+ payload: { action: "restart" },
154
+ from: "remote",
155
+ });
156
+ await waitForBus();
157
+
158
+ assert.equal(session.state.getPendingAction(), null);
159
+ assert.equal(session.state.getState("local"), beforeRestart.local);
160
+ assert.equal(session.state.getState("remote"), beforeRestart.remote);
161
+ }
162
+
163
+ {
164
+ const { client, session } = await startGame();
165
+ const snapshots = [];
166
+ session.observer.subscribe({
167
+ onStateChange(snapshot) {
168
+ snapshots.push(snapshot);
169
+ },
170
+ onGameEvent() {},
171
+ });
172
+
173
+ client.inbound({ type: "RESTART", from: "remote" });
174
+ await waitForBus();
175
+
176
+ assert.equal(session.state.getPendingAction(), "restart");
177
+ assert.equal(session.state.getState("local"), "approving");
178
+ assert.equal(session.state.getState("remote"), "waiting_approval");
179
+
180
+ session.actions.approve();
181
+ await waitForBus();
182
+
183
+ const sent = client.sent.at(-1);
184
+ assert.equal(sent.type, "APPROVE");
185
+ assert.equal(sent.payload.action, "restart");
186
+ assert.equal(session.state.getPendingAction(), null);
187
+ assert.equal(session.state.getState("local"), "idle");
188
+ assert.equal(session.state.getState("remote"), "idle");
189
+ assert.equal(
190
+ snapshots.some(
191
+ (snapshot) =>
192
+ snapshot.localState === "idle" &&
193
+ snapshot.remoteState === "idle" &&
194
+ snapshot.pendingAction === "restart",
195
+ ),
196
+ false,
197
+ );
198
+ }
199
+
200
+ {
201
+ const { client, session } = await startGame();
202
+ const beforeRestart = {
203
+ local: session.state.getState("local"),
204
+ remote: session.state.getState("remote"),
205
+ };
206
+
207
+ client.inbound({ type: "RESTART", from: "remote" });
208
+ await waitForBus();
209
+
210
+ session.actions.reject();
211
+ await waitForBus();
212
+
213
+ const sent = client.sent.at(-1);
214
+ assert.equal(sent.type, "REJECT");
215
+ assert.equal(sent.payload.action, "restart");
216
+ assert.equal(session.state.getPendingAction(), null);
217
+ assert.equal(session.state.getState("local"), beforeRestart.local);
218
+ assert.equal(session.state.getState("remote"), beforeRestart.remote);
219
+ }
220
+
84
221
  console.log("serialization smoke passed");