p2p-lockstep-kit-session 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/session/index.d.ts +28 -9
- package/dist/session/index.js +164 -29
- package/dist/session/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/serialization-smoke.mjs +118 -7
package/dist/session/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NetworkClient } from 'p2p-lockstep-kit-network';
|
|
2
2
|
|
|
3
|
-
type SessionMessageType = 'READY' | 'START' | 'MOVE' | 'UNDO' | 'RESTART' | '
|
|
3
|
+
type SessionMessageType = 'READY' | 'START' | 'MOVE' | 'UNDO' | 'RESTART' | 'REQUEST' | 'RESIGN' | 'APPROVE' | 'REJECT' | 'SYNC_REQUEST' | 'SYNC_STATE' | 'OFFLINE' | 'ONLINE' | 'GAME_OVER';
|
|
4
4
|
type SessionMessage = {
|
|
5
5
|
type: SessionMessageType;
|
|
6
6
|
from?: string;
|
|
@@ -74,7 +74,7 @@ declare class NetClient {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
type SessionState = 'idle' | 'ready' | 'could_start' | 'turn' | 'remote_turn' | 'approving' | 'waiting_approval' | 'syncing' | 'offline';
|
|
77
|
-
type SessionEvent = 'REMOTE_READY' | 'READY' | 'START' | 'REMOTE_START' | 'MOVE' | 'REMOTE_MOVE' | 'UNDO' | 'REMOTE_UNDO' | 'RESTART' | 'REMOTE_RESTART' | '
|
|
77
|
+
type SessionEvent = 'REMOTE_READY' | 'READY' | 'START' | 'REMOTE_START' | 'MOVE' | 'REMOTE_MOVE' | 'UNDO' | 'REMOTE_UNDO' | 'RESTART' | 'REMOTE_RESTART' | 'REQUEST' | 'REMOTE_REQUEST' | 'APPROVE' | 'REJECT' | 'GAME_OVER' | 'SYNC' | 'SYNC_COMPLETE' | 'OFFLINE' | 'ONLINE';
|
|
78
78
|
|
|
79
79
|
interface GameStateSnapshot {
|
|
80
80
|
localState: SessionState;
|
|
@@ -83,10 +83,11 @@ interface GameStateSnapshot {
|
|
|
83
83
|
history: TurnEntry[];
|
|
84
84
|
lastStart: PlayerLabel | null;
|
|
85
85
|
pendingAction: PendingAction;
|
|
86
|
+
outcome: GameOutcome | null;
|
|
86
87
|
connected: boolean;
|
|
87
88
|
}
|
|
88
89
|
interface GameEvent {
|
|
89
|
-
type: 'READY' | 'START' | 'MOVE' | 'GAME_OVER' | 'UNDO' | 'RESTART' | 'OFFLINE' | 'ONLINE' | 'SYNC' | 'ERROR';
|
|
90
|
+
type: 'READY' | 'START' | 'MOVE' | 'GAME_OVER' | 'UNDO' | 'RESTART' | 'DRAW' | 'RESIGN' | 'OFFLINE' | 'ONLINE' | 'SYNC' | 'ERROR';
|
|
90
91
|
payload?: unknown;
|
|
91
92
|
from?: 'local' | 'remote';
|
|
92
93
|
timestamp?: number;
|
|
@@ -169,7 +170,16 @@ type TurnEntry = {
|
|
|
169
170
|
move?: unknown;
|
|
170
171
|
};
|
|
171
172
|
type PlayerLabel = 'local' | 'remote';
|
|
172
|
-
type
|
|
173
|
+
type PendingActionId = 'undo' | 'restart' | 'draw';
|
|
174
|
+
type PendingAction = PendingActionId | null;
|
|
175
|
+
type GameOutcome = {
|
|
176
|
+
kind: 'win';
|
|
177
|
+
winner: PlayerLabel;
|
|
178
|
+
reason: 'rules' | 'resignation';
|
|
179
|
+
} | {
|
|
180
|
+
kind: 'draw';
|
|
181
|
+
reason: 'agreement' | 'mutual_resignation';
|
|
182
|
+
};
|
|
173
183
|
declare class State {
|
|
174
184
|
private local;
|
|
175
185
|
private remote;
|
|
@@ -180,6 +190,7 @@ declare class State {
|
|
|
180
190
|
private pendingUndoCount;
|
|
181
191
|
private resumeTurn;
|
|
182
192
|
private lastStart;
|
|
193
|
+
private outcome;
|
|
183
194
|
private gamePlugin;
|
|
184
195
|
private stateObserverManager;
|
|
185
196
|
constructor(id: string | null, remoteId: string | null);
|
|
@@ -215,6 +226,8 @@ declare class State {
|
|
|
215
226
|
getLastStart(): PlayerLabel | null;
|
|
216
227
|
setResumeTurn(player: PlayerLabel | null): void;
|
|
217
228
|
getResumeTurn(): PlayerLabel | null;
|
|
229
|
+
setOutcome(outcome: GameOutcome | null): void;
|
|
230
|
+
getOutcome(): GameOutcome | null;
|
|
218
231
|
private getPlayerFsm;
|
|
219
232
|
private notifyStateChanged;
|
|
220
233
|
private notifyHistoryChanged;
|
|
@@ -243,6 +256,7 @@ declare class State {
|
|
|
243
256
|
* Initialize restart request with resume turn
|
|
244
257
|
*/
|
|
245
258
|
initializeRestartRequest(resumeTurn: PlayerLabel): void;
|
|
259
|
+
initializePendingRequest(action: Exclude<PendingActionId, 'undo' | 'restart'>, resumeTurn: PlayerLabel): void;
|
|
246
260
|
/**
|
|
247
261
|
* Check if pending action is undo
|
|
248
262
|
*/
|
|
@@ -260,7 +274,7 @@ declare class State {
|
|
|
260
274
|
*/
|
|
261
275
|
resetGame(): void;
|
|
262
276
|
/**
|
|
263
|
-
* Save start player for
|
|
277
|
+
* Save start player for reconnect synchronization
|
|
264
278
|
*/
|
|
265
279
|
recordStartPlayer(player: PlayerLabel): void;
|
|
266
280
|
/**
|
|
@@ -309,6 +323,7 @@ declare class State {
|
|
|
309
323
|
* @returns Winner (local/remote) or null if game continues
|
|
310
324
|
*/
|
|
311
325
|
checkWin(): PlayerLabel | null;
|
|
326
|
+
completeGame(outcome: GameOutcome): void;
|
|
312
327
|
/**
|
|
313
328
|
* Cleanup when game ends (for plugin to reset internal state)
|
|
314
329
|
*/
|
|
@@ -326,9 +341,11 @@ interface ISessionActions {
|
|
|
326
341
|
move(data: unknown): void;
|
|
327
342
|
undo(): void;
|
|
328
343
|
restart(): void;
|
|
344
|
+
request(action: PendingActionId, payload?: unknown): void;
|
|
345
|
+
offerDraw(): void;
|
|
346
|
+
resign(): void;
|
|
329
347
|
approve(): void;
|
|
330
348
|
reject(): void;
|
|
331
|
-
rejoin(sid: string): void;
|
|
332
349
|
}
|
|
333
350
|
declare class LocalActionsAPI implements ISessionActions {
|
|
334
351
|
private bus;
|
|
@@ -338,14 +355,16 @@ declare class LocalActionsAPI implements ISessionActions {
|
|
|
338
355
|
move(data: unknown): void;
|
|
339
356
|
undo(): void;
|
|
340
357
|
restart(): void;
|
|
358
|
+
request(action: PendingActionId, payload?: unknown): void;
|
|
359
|
+
offerDraw(): void;
|
|
360
|
+
resign(): void;
|
|
341
361
|
approve(): void;
|
|
342
362
|
reject(): void;
|
|
343
|
-
rejoin(sid: string): void;
|
|
344
363
|
}
|
|
345
364
|
|
|
346
365
|
/**
|
|
347
366
|
* Create a new game session with state management and networking
|
|
348
|
-
* @param sid
|
|
367
|
+
* @param sid Protocol scope identifier shared by both peers (optional)
|
|
349
368
|
* @param networkClient Custom network client (optional, creates default if not provided)
|
|
350
369
|
* @returns Session manager with bus, state, observer, net, and send method
|
|
351
370
|
*
|
|
@@ -365,4 +384,4 @@ declare const createSession: (networkClient: NetworkClient, sid?: string) => {
|
|
|
365
384
|
send: (message: SessionMessage) => void;
|
|
366
385
|
};
|
|
367
386
|
|
|
368
|
-
export { DefaultGamePlugin, type GameEvent, type GameState, GameStateObserver, type GameStateSnapshot, type IGameObserver, type IGamePlugin, type ISessionActions, type IStateObserver, type PendingAction, type PlayerLabel, type SessionEvent, type SessionState, StateObserverManager, type TurnEntry, UINotificationAdapter, type ValidationResult, buildGameStateSnapshot, createSession };
|
|
387
|
+
export { DefaultGamePlugin, type GameEvent, type GameOutcome, type GameState, GameStateObserver, type GameStateSnapshot, type IGameObserver, type IGamePlugin, type ISessionActions, type IStateObserver, type PendingAction, type PendingActionId, type PlayerLabel, type SessionEvent, type SessionState, StateObserverManager, type TurnEntry, UINotificationAdapter, type ValidationResult, buildGameStateSnapshot, createSession };
|
package/dist/session/index.js
CHANGED
|
@@ -92,7 +92,7 @@ var CommandBus = class {
|
|
|
92
92
|
|
|
93
93
|
// session/state/fsm.ts
|
|
94
94
|
var transitions = [
|
|
95
|
-
//
|
|
95
|
+
// Pre-match readiness
|
|
96
96
|
{ from: "idle", event: "READY", to: "ready" },
|
|
97
97
|
{ from: "ready", event: "READY", to: "idle" },
|
|
98
98
|
{ from: "idle", event: "REMOTE_READY", to: "could_start" },
|
|
@@ -114,11 +114,15 @@ var transitions = [
|
|
|
114
114
|
{ from: "remote_turn", event: "UNDO", to: "waiting_approval" },
|
|
115
115
|
{ from: "turn", event: "RESTART", to: "waiting_approval" },
|
|
116
116
|
{ from: "remote_turn", event: "RESTART", to: "waiting_approval" },
|
|
117
|
+
{ from: "turn", event: "REQUEST", to: "waiting_approval" },
|
|
118
|
+
{ from: "remote_turn", event: "REQUEST", to: "waiting_approval" },
|
|
117
119
|
// Requests coming from remote (we need to approve)
|
|
118
120
|
{ from: "turn", event: "REMOTE_UNDO", to: "approving" },
|
|
119
121
|
{ from: "remote_turn", event: "REMOTE_UNDO", to: "approving" },
|
|
120
122
|
{ from: "turn", event: "REMOTE_RESTART", to: "approving" },
|
|
121
123
|
{ from: "remote_turn", event: "REMOTE_RESTART", to: "approving" },
|
|
124
|
+
{ from: "turn", event: "REMOTE_REQUEST", to: "approving" },
|
|
125
|
+
{ from: "remote_turn", event: "REMOTE_REQUEST", to: "approving" },
|
|
122
126
|
// Approval outcomes when we were waiting for the peer.
|
|
123
127
|
{ from: "waiting_approval", event: "APPROVE", to: "turn" },
|
|
124
128
|
{ from: "waiting_approval", event: "REJECT", to: "turn" },
|
|
@@ -128,9 +132,12 @@ var transitions = [
|
|
|
128
132
|
{ from: "approving", event: "APPROVE", to: "remote_turn" },
|
|
129
133
|
{ from: "approving", event: "REJECT", to: "remote_turn" },
|
|
130
134
|
{ from: "approving", event: "REJECT", to: "turn" },
|
|
131
|
-
// Game end resets back to
|
|
135
|
+
// Game end resets back to the pre-match idle state
|
|
132
136
|
{ from: "turn", event: "GAME_OVER", to: "idle" },
|
|
133
137
|
{ from: "remote_turn", event: "GAME_OVER", to: "idle" },
|
|
138
|
+
{ from: "waiting_approval", event: "GAME_OVER", to: "idle" },
|
|
139
|
+
{ from: "approving", event: "GAME_OVER", to: "idle" },
|
|
140
|
+
{ from: "syncing", event: "GAME_OVER", to: "idle" },
|
|
134
141
|
// Rejoin/sync flows
|
|
135
142
|
{ from: "turn", event: "SYNC", to: "syncing" },
|
|
136
143
|
{ from: "remote_turn", event: "SYNC", to: "syncing" },
|
|
@@ -304,6 +311,7 @@ function buildGameStateSnapshot(state, connected = false) {
|
|
|
304
311
|
history: state.getHistory(),
|
|
305
312
|
lastStart: state.getLastStart(),
|
|
306
313
|
pendingAction: state.getPendingAction(),
|
|
314
|
+
outcome: state.getOutcome(),
|
|
307
315
|
connected
|
|
308
316
|
};
|
|
309
317
|
}
|
|
@@ -357,11 +365,12 @@ var State = class {
|
|
|
357
365
|
__publicField(this, "pendingUndoCount", null);
|
|
358
366
|
__publicField(this, "resumeTurn", null);
|
|
359
367
|
__publicField(this, "lastStart", null);
|
|
368
|
+
__publicField(this, "outcome", null);
|
|
360
369
|
// Game plugin for rule validation and win checking
|
|
361
370
|
__publicField(this, "gamePlugin", new DefaultGamePlugin());
|
|
362
371
|
// Internal state observer for UI notifications
|
|
363
372
|
__publicField(this, "stateObserverManager", new StateObserverManager());
|
|
364
|
-
// ===== Helper Methods for
|
|
373
|
+
// ===== Helper Methods for Approval Request Handling =====
|
|
365
374
|
/**
|
|
366
375
|
* Save game state snapshot for undo/restart operations
|
|
367
376
|
*/
|
|
@@ -507,6 +516,17 @@ var State = class {
|
|
|
507
516
|
getResumeTurn() {
|
|
508
517
|
return this.resumeTurn;
|
|
509
518
|
}
|
|
519
|
+
setOutcome(outcome) {
|
|
520
|
+
consoleLogger.debug("[session:state] outcome set", {
|
|
521
|
+
from: this.outcome,
|
|
522
|
+
to: outcome
|
|
523
|
+
});
|
|
524
|
+
this.outcome = outcome;
|
|
525
|
+
this.notifyStateChanged();
|
|
526
|
+
}
|
|
527
|
+
getOutcome() {
|
|
528
|
+
return this.outcome;
|
|
529
|
+
}
|
|
510
530
|
getPlayerFsm(player) {
|
|
511
531
|
return player === "local" ? this.local : this.remote;
|
|
512
532
|
}
|
|
@@ -608,6 +628,14 @@ var State = class {
|
|
|
608
628
|
resumeTurn
|
|
609
629
|
});
|
|
610
630
|
}
|
|
631
|
+
initializePendingRequest(action, resumeTurn) {
|
|
632
|
+
this.pendingAction = action;
|
|
633
|
+
this.resumeTurn = resumeTurn;
|
|
634
|
+
consoleLogger.debug("[session:state] approval request initialized", {
|
|
635
|
+
action,
|
|
636
|
+
resumeTurn
|
|
637
|
+
});
|
|
638
|
+
}
|
|
611
639
|
/**
|
|
612
640
|
* Check if pending action is undo
|
|
613
641
|
*/
|
|
@@ -644,6 +672,7 @@ var State = class {
|
|
|
644
672
|
this.local = new SessionFsm("idle");
|
|
645
673
|
this.remote = new SessionFsm("idle");
|
|
646
674
|
this.lastStart = null;
|
|
675
|
+
this.outcome = null;
|
|
647
676
|
this.pendingAction = null;
|
|
648
677
|
this.pendingUndoCount = null;
|
|
649
678
|
this.resumeTurn = null;
|
|
@@ -651,7 +680,7 @@ var State = class {
|
|
|
651
680
|
this.notifyStateChanged();
|
|
652
681
|
}
|
|
653
682
|
/**
|
|
654
|
-
* Save start player for
|
|
683
|
+
* Save start player for reconnect synchronization
|
|
655
684
|
*/
|
|
656
685
|
recordStartPlayer(player) {
|
|
657
686
|
this.lastStart = player;
|
|
@@ -795,6 +824,17 @@ var State = class {
|
|
|
795
824
|
});
|
|
796
825
|
return winner;
|
|
797
826
|
}
|
|
827
|
+
completeGame(outcome) {
|
|
828
|
+
this.setOutcome(outcome);
|
|
829
|
+
if (this.canAction("local", "GAME_OVER")) {
|
|
830
|
+
this.dispatch("local", "GAME_OVER");
|
|
831
|
+
}
|
|
832
|
+
if (this.canAction("remote", "GAME_OVER")) {
|
|
833
|
+
this.dispatch("remote", "GAME_OVER");
|
|
834
|
+
}
|
|
835
|
+
this.clearPendingStates();
|
|
836
|
+
this.cleanupGame();
|
|
837
|
+
}
|
|
798
838
|
/**
|
|
799
839
|
* Cleanup when game ends (for plugin to reset internal state)
|
|
800
840
|
*/
|
|
@@ -1046,6 +1086,7 @@ var start = (command) => {
|
|
|
1046
1086
|
state.clearHistory();
|
|
1047
1087
|
consoleLogger.debug("[session:start] cleared previous match history");
|
|
1048
1088
|
}
|
|
1089
|
+
state.setOutcome(null);
|
|
1049
1090
|
state.setLastStart(nextStarter);
|
|
1050
1091
|
state.dispatch("local", "START", localTarget2);
|
|
1051
1092
|
state.dispatch("remote", "REMOTE_START", remoteTarget2);
|
|
@@ -1075,6 +1116,7 @@ var start = (command) => {
|
|
|
1075
1116
|
state.clearHistory();
|
|
1076
1117
|
consoleLogger.debug("[session:start] cleared previous match history");
|
|
1077
1118
|
}
|
|
1119
|
+
state.setOutcome(null);
|
|
1078
1120
|
state.setLastStart(starter);
|
|
1079
1121
|
state.dispatch("local", "REMOTE_START", localTarget);
|
|
1080
1122
|
state.dispatch("remote", "START", remoteTarget);
|
|
@@ -1132,9 +1174,7 @@ var move = (command) => {
|
|
|
1132
1174
|
winner: winner2,
|
|
1133
1175
|
turn: turn2
|
|
1134
1176
|
});
|
|
1135
|
-
state.
|
|
1136
|
-
state.dispatch("remote", "GAME_OVER");
|
|
1137
|
-
state.cleanupGame();
|
|
1177
|
+
state.completeGame({ kind: "win", winner: winner2, reason: "rules" });
|
|
1138
1178
|
consoleLogger.debug("[session:move] local game over applied", {
|
|
1139
1179
|
winner: winner2,
|
|
1140
1180
|
turn: turn2
|
|
@@ -1168,9 +1208,7 @@ var move = (command) => {
|
|
|
1168
1208
|
const winner = state.checkWin();
|
|
1169
1209
|
if (winner) {
|
|
1170
1210
|
consoleLogger.debug("[session:move] game over detected", { winner, turn });
|
|
1171
|
-
state.
|
|
1172
|
-
state.dispatch("remote", "GAME_OVER");
|
|
1173
|
-
state.cleanupGame();
|
|
1211
|
+
state.completeGame({ kind: "win", winner, reason: "rules" });
|
|
1174
1212
|
consoleLogger.debug("[session:move] remote game over applied", {
|
|
1175
1213
|
winner,
|
|
1176
1214
|
turn
|
|
@@ -1184,7 +1222,17 @@ var move = (command) => {
|
|
|
1184
1222
|
};
|
|
1185
1223
|
|
|
1186
1224
|
// session/handlers/request.ts
|
|
1187
|
-
var isRequestAction = (value) => value === "undo" || value === "restart";
|
|
1225
|
+
var isRequestAction = (value) => value === "undo" || value === "restart" || value === "draw";
|
|
1226
|
+
var applyApprovedAction = (action) => {
|
|
1227
|
+
const state = getState();
|
|
1228
|
+
if (action === "undo") {
|
|
1229
|
+
state.applyUndo(state.getPendingUndoCount() ?? 1);
|
|
1230
|
+
} else if (action === "restart") {
|
|
1231
|
+
state.resetGame();
|
|
1232
|
+
} else {
|
|
1233
|
+
state.completeGame({ kind: "draw", reason: "agreement" });
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1188
1236
|
var request = (command) => {
|
|
1189
1237
|
if (command.type !== "APPROVE" && command.type !== "REJECT") {
|
|
1190
1238
|
return;
|
|
@@ -1230,11 +1278,7 @@ var request = (command) => {
|
|
|
1230
1278
|
return;
|
|
1231
1279
|
}
|
|
1232
1280
|
state.dispatchApprove();
|
|
1233
|
-
|
|
1234
|
-
state.applyUndo(state.getPendingUndoCount() ?? 1);
|
|
1235
|
-
} else if (action === "restart") {
|
|
1236
|
-
state.resetGame();
|
|
1237
|
-
}
|
|
1281
|
+
applyApprovedAction(action);
|
|
1238
1282
|
send({ type: "APPROVE", payload: { action } });
|
|
1239
1283
|
state.clearPendingStates();
|
|
1240
1284
|
consoleLogger.debug("[session:request] local approved", { action });
|
|
@@ -1261,11 +1305,7 @@ var request = (command) => {
|
|
|
1261
1305
|
return;
|
|
1262
1306
|
}
|
|
1263
1307
|
state.dispatchApprove();
|
|
1264
|
-
|
|
1265
|
-
state.applyUndo(state.getPendingUndoCount() ?? 1);
|
|
1266
|
-
} else if (action === "restart") {
|
|
1267
|
-
state.resetGame();
|
|
1268
|
-
}
|
|
1308
|
+
applyApprovedAction(action);
|
|
1269
1309
|
state.clearPendingStates();
|
|
1270
1310
|
consoleLogger.debug("[session:request] remote approved", { action });
|
|
1271
1311
|
return;
|
|
@@ -1295,7 +1335,8 @@ var buildSyncPayload = () => {
|
|
|
1295
1335
|
history: state.getHistory(),
|
|
1296
1336
|
lastStart: state.getLastStart(),
|
|
1297
1337
|
turn,
|
|
1298
|
-
resumeTurn: state.getResumeTurn()
|
|
1338
|
+
resumeTurn: state.getResumeTurn(),
|
|
1339
|
+
outcome: state.getOutcome()
|
|
1299
1340
|
};
|
|
1300
1341
|
};
|
|
1301
1342
|
var isInSyncRecovery = () => {
|
|
@@ -1350,6 +1391,8 @@ var restoreFromPayload = (payload, mapPeerLabels) => {
|
|
|
1350
1391
|
} else {
|
|
1351
1392
|
state.setLastStart(null);
|
|
1352
1393
|
}
|
|
1394
|
+
const mappedOutcome = payload.outcome ? payload.outcome.kind === "win" ? { ...payload.outcome, winner: mapPlayer(payload.outcome.winner) } : payload.outcome : null;
|
|
1395
|
+
state.setOutcome(mappedOutcome);
|
|
1353
1396
|
const nextPlayer = payload.resumeTurn ? mapPlayer(payload.resumeTurn) : payload.turn ? mapPlayer(payload.turn) : getCurrentTurn();
|
|
1354
1397
|
consoleLogger.debug("[session:sync] state restored", {
|
|
1355
1398
|
historyLength: state.getHistory().length,
|
|
@@ -1360,7 +1403,12 @@ var restoreFromPayload = (payload, mapPeerLabels) => {
|
|
|
1360
1403
|
if (!enterSyncState()) {
|
|
1361
1404
|
return;
|
|
1362
1405
|
}
|
|
1363
|
-
|
|
1406
|
+
if (mappedOutcome) {
|
|
1407
|
+
state.dispatch("local", "GAME_OVER");
|
|
1408
|
+
state.dispatch("remote", "GAME_OVER");
|
|
1409
|
+
} else {
|
|
1410
|
+
state.dispatchSyncComplete(nextPlayer);
|
|
1411
|
+
}
|
|
1364
1412
|
};
|
|
1365
1413
|
var sync = (command) => {
|
|
1366
1414
|
const state = getState();
|
|
@@ -1551,10 +1599,10 @@ var offline = (command) => {
|
|
|
1551
1599
|
console.warn("[Offline] Cannot transition to OFFLINE from current state");
|
|
1552
1600
|
return;
|
|
1553
1601
|
}
|
|
1554
|
-
const
|
|
1555
|
-
state.setResumeTurn(
|
|
1602
|
+
const currentTurn2 = state.getResumeTurn() ?? (state.getState("local") === "turn" ? "local" : "remote");
|
|
1603
|
+
state.setResumeTurn(currentTurn2);
|
|
1556
1604
|
state.dispatch("remote", "OFFLINE", "offline");
|
|
1557
|
-
consoleLogger.debug("[session:connection] remote offline", { currentTurn });
|
|
1605
|
+
consoleLogger.debug("[session:connection] remote offline", { currentTurn: currentTurn2 });
|
|
1558
1606
|
return;
|
|
1559
1607
|
}
|
|
1560
1608
|
if (state.getState("remote") !== "offline") {
|
|
@@ -1575,6 +1623,85 @@ var offline = (command) => {
|
|
|
1575
1623
|
consoleLogger.debug("[session:connection] remote online, sync state pushed");
|
|
1576
1624
|
};
|
|
1577
1625
|
|
|
1626
|
+
// session/handlers/pendingRequest.ts
|
|
1627
|
+
var isGenericRequestAction = (value) => value === "draw";
|
|
1628
|
+
var currentTurn = () => getState().getState("local") === "turn" ? "local" : "remote";
|
|
1629
|
+
var pendingRequest = (command) => {
|
|
1630
|
+
if (command.type !== "REQUEST") return;
|
|
1631
|
+
const state = getState();
|
|
1632
|
+
const bus = getBus();
|
|
1633
|
+
const payload = command.payload;
|
|
1634
|
+
const action = payload?.action;
|
|
1635
|
+
if (!isGenericRequestAction(action)) {
|
|
1636
|
+
if (command.from === "remote") {
|
|
1637
|
+
send({
|
|
1638
|
+
type: "REJECT",
|
|
1639
|
+
payload: { action, reason: "unknown_action" }
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (state.getOutcome()) return;
|
|
1645
|
+
if (command.from === "local") {
|
|
1646
|
+
if (state.hasPendingAction() || !state.canAction("local", "REQUEST")) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const resumeTurn2 = currentTurn();
|
|
1650
|
+
state.initializePendingRequest(action, resumeTurn2);
|
|
1651
|
+
state.dispatch("local", "REQUEST");
|
|
1652
|
+
state.dispatch("remote", "REMOTE_REQUEST");
|
|
1653
|
+
send({ type: "REQUEST", payload: { action, payload: payload?.payload } });
|
|
1654
|
+
consoleLogger.debug("[session:request] local requested", { action });
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if (state.hasPendingAction()) {
|
|
1658
|
+
bus.emit("REJECT", { action, reason: "busy" }, "local");
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (!state.canAction("local", "REMOTE_REQUEST")) {
|
|
1662
|
+
bus.emit("REJECT", { action, reason: "invalid_state" }, "local");
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
const resumeTurn = currentTurn();
|
|
1666
|
+
state.initializePendingRequest(action, resumeTurn);
|
|
1667
|
+
state.dispatch("local", "REMOTE_REQUEST");
|
|
1668
|
+
state.dispatch("remote", "REQUEST");
|
|
1669
|
+
consoleLogger.debug("[session:request] remote requested", { action });
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
// session/handlers/resign.ts
|
|
1673
|
+
var resign = (command) => {
|
|
1674
|
+
if (command.type !== "RESIGN") return;
|
|
1675
|
+
const state = getState();
|
|
1676
|
+
const previous = state.getOutcome();
|
|
1677
|
+
if (previous?.kind === "win" && previous.reason === "resignation") {
|
|
1678
|
+
state.setOutcome({ kind: "draw", reason: "mutual_resignation" });
|
|
1679
|
+
consoleLogger.debug("[session:resign] simultaneous resignation resolved");
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
if (previous || state.hasPendingAction()) return;
|
|
1683
|
+
const active = state.getState("local") === "turn" || state.getState("local") === "remote_turn";
|
|
1684
|
+
if (!active) return;
|
|
1685
|
+
if (command.from === "local") {
|
|
1686
|
+
send({ type: "RESIGN" });
|
|
1687
|
+
state.completeGame({
|
|
1688
|
+
kind: "win",
|
|
1689
|
+
winner: "remote",
|
|
1690
|
+
reason: "resignation"
|
|
1691
|
+
});
|
|
1692
|
+
} else {
|
|
1693
|
+
state.completeGame({
|
|
1694
|
+
kind: "win",
|
|
1695
|
+
winner: "local",
|
|
1696
|
+
reason: "resignation"
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
consoleLogger.debug("[session:resign] game completed", {
|
|
1700
|
+
from: command.from,
|
|
1701
|
+
outcome: state.getOutcome()
|
|
1702
|
+
});
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1578
1705
|
// session/handlers/busRegister.ts
|
|
1579
1706
|
var registerHandlers = (bus) => {
|
|
1580
1707
|
bus.register("READY", ready);
|
|
@@ -1582,6 +1709,8 @@ var registerHandlers = (bus) => {
|
|
|
1582
1709
|
bus.register("MOVE", move);
|
|
1583
1710
|
bus.register("UNDO", undo);
|
|
1584
1711
|
bus.register("RESTART", restart);
|
|
1712
|
+
bus.register("REQUEST", pendingRequest);
|
|
1713
|
+
bus.register("RESIGN", resign);
|
|
1585
1714
|
bus.register("SYNC_REQUEST", sync);
|
|
1586
1715
|
bus.register("SYNC_STATE", sync);
|
|
1587
1716
|
bus.register("OFFLINE", offline);
|
|
@@ -1610,15 +1739,21 @@ var LocalActionsAPI = class {
|
|
|
1610
1739
|
restart() {
|
|
1611
1740
|
this.bus.emit("RESTART");
|
|
1612
1741
|
}
|
|
1742
|
+
request(action, payload) {
|
|
1743
|
+
this.bus.emit("REQUEST", { action, payload });
|
|
1744
|
+
}
|
|
1745
|
+
offerDraw() {
|
|
1746
|
+
this.request("draw");
|
|
1747
|
+
}
|
|
1748
|
+
resign() {
|
|
1749
|
+
this.bus.emit("RESIGN");
|
|
1750
|
+
}
|
|
1613
1751
|
approve() {
|
|
1614
1752
|
this.bus.emit("APPROVE");
|
|
1615
1753
|
}
|
|
1616
1754
|
reject() {
|
|
1617
1755
|
this.bus.emit("REJECT");
|
|
1618
1756
|
}
|
|
1619
|
-
rejoin(sid) {
|
|
1620
|
-
this.bus.emit("REJOIN", { sid });
|
|
1621
|
-
}
|
|
1622
1757
|
};
|
|
1623
1758
|
|
|
1624
1759
|
// session/index.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../utils/logger.ts","../../utils/serialization/json.ts","../../utils/protocol/session.ts","../../session/commandBus.ts","../../session/state/fsm.ts","../../session/observer/index.ts","../../session/state/state.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":["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 const write = level === 'debug' ? console.info : console[level];\n if (meta !== undefined) {\n // eslint-disable-next-line no-console\n write(message, meta);\n return;\n }\n // eslint-disable-next-line no-console\n write(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 { consoleLogger, SessionMessage, SessionMessageType } from '../utils';\n\nexport type CommandOrigin = 'local' | 'remote';\nexport type BusMessageType =\n | SessionMessageType\n | 'OFFLINE'\n | 'ONLINE'\n | 'GAME_OVER';\nexport type BusMessage = Omit<SessionMessage, 'type'> & {\n type: BusMessageType;\n};\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(\n type: BusMessageType,\n payload?: unknown,\n from: CommandOrigin = 'local',\n ): void {\n this.dispatch({ type, payload, from });\n }\n\n public register(type: BusMessageType, handler: CommandListener): void {\n this.handlers[type] = handler;\n consoleLogger.debug(`[session:bus] registered ${type}`);\n }\n\n public dispatch(message: BusMessage): void {\n this.processingQueue = this.processingQueue.then(async () => {\n consoleLogger.debug(`[session:bus] dispatch ${message.type}`, {\n from: message.from,\n payload: message.payload,\n turn: message.turn,\n sid: message.sid,\n });\n\n const handler = this.handlers[message.type];\n if (handler) {\n try {\n await handler(message);\n consoleLogger.debug(`[session:bus] handled ${message.type}`, {\n from: message.from,\n });\n } catch (err) {\n console.error(`[CommandBus] Error in ${message.type}:`, err);\n }\n return;\n }\n\n consoleLogger.debug(`[session:bus] no handler for ${message.type}`, {\n from: message.from,\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 for the peer.\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 the peer's request.\n // The target turn is explicit because resumeTurn decides who continues.\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 {\n PendingAction,\n PlayerLabel,\n TurnEntry,\n State,\n} from '../state/state';\nimport type { SessionState } from '../state/fsm';\nimport { consoleLogger } from '../../utils';\n\nexport interface GameStateSnapshot {\n localState: SessionState;\n remoteState: SessionState;\n turn: number;\n history: TurnEntry[];\n lastStart: PlayerLabel | null;\n pendingAction: PendingAction;\n connected: boolean;\n}\n\nexport interface GameEvent {\n type:\n | 'READY'\n | 'START'\n | 'MOVE'\n | 'GAME_OVER'\n | 'UNDO'\n | 'RESTART'\n | 'OFFLINE'\n | 'ONLINE'\n | 'SYNC'\n | 'ERROR';\n payload?: unknown;\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?: unknown }): 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?: unknown }): 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(\n state: State,\n connected: boolean = false,\n): 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 consoleLogger.debug('[session:observer] state snapshot', {\n local: snapshot.localState,\n remote: snapshot.remoteState,\n turn: snapshot.turn,\n history: snapshot.history.length,\n pending: snapshot.pendingAction,\n connected: snapshot.connected,\n });\n this.uiObserver.notifyStateChange(snapshot);\n }\n\n onHistoryChanged(): void {\n this.onStateChanged();\n }\n\n onGameReset(): void {\n this.onStateChanged();\n }\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';\nimport { consoleLogger } from '../../utils';\n\nexport type TurnEntry = {\n turn: number;\n player: 'local' | 'remote';\n move?: unknown;\n};\n\nexport type PlayerLabel = 'local' | 'remote';\nexport type PendingAction = 'undo' | 'restart' | null;\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: PendingAction = 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 consoleLogger.debug('[session:state] created', { localId: id, remoteId });\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 consoleLogger.debug('[session:state] remote id set', { 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 consoleLogger.debug('[session:history] replace', { count: entries.length });\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 const count = this.history.length;\n this.history.splice(0, this.history.length);\n consoleLogger.debug('[session:history] clear', { count });\n this.notifyHistoryChanged();\n }\n\n public pushHistory(entry: TurnEntry): void {\n this.history.push(entry);\n consoleLogger.debug('[session:history] push', {\n turn: entry.turn,\n player: entry.player,\n move: entry.move,\n count: this.history.length,\n });\n this.notifyHistoryChanged();\n }\n\n public popHistory(): TurnEntry | null {\n const entry = this.history.pop() ?? null;\n if (entry) {\n consoleLogger.debug('[session:history] pop', {\n turn: entry.turn,\n player: entry.player,\n move: entry.move,\n count: this.history.length,\n });\n this.notifyHistoryChanged();\n }\n return entry;\n }\n\n public canAction(player: PlayerLabel, action: SessionEvent): 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 const before = this.getState(player);\n this.getPlayerFsm(player).dispatch(action, to);\n const after = this.getState(player);\n consoleLogger.debug(`[session:fsm] ${player} ${action}`, {\n from: before,\n to: after,\n requested: to,\n local: this.getState('local'),\n remote: this.getState('remote'),\n turn: this.getTurnCount(),\n history: this.history.length,\n pending: this.pendingAction,\n });\n this.notifyStateChanged();\n }\n\n public setPendingAction(action: PendingAction) {\n consoleLogger.debug('[session:state] pending action set', {\n from: this.pendingAction,\n to: action,\n });\n this.pendingAction = action;\n }\n\n public getPendingAction(): PendingAction {\n return this.pendingAction;\n }\n\n public setPendingUndoCount(count: 1 | 2 | null) {\n consoleLogger.debug('[session:state] pending undo count set', {\n from: this.pendingUndoCount,\n to: count,\n });\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 consoleLogger.debug('[session:state] last start set', {\n from: this.lastStart,\n to: player,\n });\n this.lastStart = player;\n }\n\n public getLastStart(): PlayerLabel | null {\n return this.lastStart;\n }\n\n public setResumeTurn(player: PlayerLabel | null) {\n consoleLogger.debug('[session:state] resume turn set', {\n from: this.resumeTurn,\n to: player,\n });\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 consoleLogger.debug('[session:state] notify state changed', {\n local: this.getState('local'),\n remote: this.getState('remote'),\n turn: this.getTurnCount(),\n history: this.history.length,\n pending: this.pendingAction,\n });\n this.stateObserverManager.notifyStateChanged();\n }\n\n private notifyHistoryChanged(): void {\n consoleLogger.debug('[session:state] notify history changed', {\n turn: this.getTurnCount(),\n history: this.history.length,\n pending: this.pendingAction,\n });\n this.stateObserverManager.notifyHistoryChanged();\n }\n\n private notifyGameReset(): void {\n consoleLogger.debug('[session:state] notify game reset');\n this.stateObserverManager.notifyGameReset();\n }\n\n private dispatchPair(\n localAction: SessionEvent,\n localTo: SessionState,\n remoteAction: SessionEvent,\n remoteTo: SessionState,\n ): void {\n const before = {\n local: this.local.getState(),\n remote: this.remote.getState(),\n };\n this.local.dispatch(localAction, localTo);\n this.remote.dispatch(remoteAction, remoteTo);\n consoleLogger.debug('[session:fsm] pair dispatch', {\n before,\n after: {\n local: this.local.getState(),\n remote: this.remote.getState(),\n },\n localAction,\n localTo,\n remoteAction,\n remoteTo,\n turn: this.getTurnCount(),\n history: this.history.length,\n pending: this.pendingAction,\n });\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 consoleLogger.debug('[session:state] game snapshot saved', { snapshot });\n }\n\n public getGameSnapshot(): unknown {\n return this.gameSnapshot;\n }\n\n public clearGameSnapshot(): void {\n this.gameSnapshot = null;\n consoleLogger.debug('[session:state] game snapshot cleared');\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 consoleLogger.debug('[session:state] pending states cleared', {\n pending: this.pendingAction,\n pendingUndoCount: this.pendingUndoCount,\n resumeTurn: this.resumeTurn,\n });\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(\n undoCount: 1 | 2,\n resumeTurn: PlayerLabel,\n ): void {\n this.pendingAction = 'undo';\n this.pendingUndoCount = undoCount;\n this.resumeTurn = resumeTurn;\n consoleLogger.debug('[session:state] undo request initialized', {\n undoCount,\n resumeTurn,\n });\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 consoleLogger.debug('[session:state] restart request initialized', {\n resumeTurn,\n });\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 consoleLogger.debug('[session:history] apply undo', { count });\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 consoleLogger.debug('[session:state] reset game', {\n local: this.getState('local'),\n remote: this.getState('remote'),\n history: this.history.length,\n lastStart: this.lastStart,\n pending: this.pendingAction,\n });\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 consoleLogger.debug('[session:state] start player recorded', { player });\n }\n\n /**\n * Get move to undo from history\n */\n public getLastMove(): TurnEntry | null {\n return this.history.length > 0\n ? this.history[this.history.length - 1]\n : 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 const before = {\n local: this.local.getState(),\n remote: this.remote.getState(),\n lastStart: this.lastStart,\n };\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 consoleLogger.debug('[session:fsm] start dispatch', {\n before,\n firstPlayer,\n after: {\n local: this.local.getState(),\n remote: this.remote.getState(),\n lastStart: this.lastStart,\n },\n });\n this.notifyStateChanged();\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.dispatchPair(\n 'SYNC_COMPLETE',\n 'turn',\n 'SYNC_COMPLETE',\n 'remote_turn',\n );\n } else {\n this.dispatchPair(\n 'SYNC_COMPLETE',\n 'remote_turn',\n 'SYNC_COMPLETE',\n 'turn',\n );\n }\n this.resumeTurn = null;\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 consoleLogger.debug('[session:plugin] game plugin set', {\n hasInitialize: Boolean(plugin.initialize),\n hasCleanup: Boolean(plugin.cleanup),\n });\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 const result = this.gamePlugin.validateMove(move, gameState);\n consoleLogger.debug('[session:plugin] validate move', {\n move,\n result,\n local: gameState.localState,\n remote: gameState.remoteState,\n turn: gameState.turn,\n history: gameState.history.length,\n });\n return result;\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 const winner = this.gamePlugin.checkWin(gameState, this.getHistory());\n consoleLogger.debug('[session:plugin] check win', {\n winner,\n turn: gameState.turn,\n history: gameState.history.length,\n });\n return winner;\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 consoleLogger.debug('[session:plugin] cleanup game');\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","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 { consoleLogger, 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 consoleLogger.debug('[session:ready] received', {\n from: command.from,\n sid: (command as any).sid,\n localSid,\n local: state.getState('local'),\n remote: state.getState('remote'),\n });\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 consoleLogger.debug('[session:ready] local toggled', {\n local: state.getState('local'),\n remote: state.getState('remote'),\n });\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 consoleLogger.debug('[session:ready] remote toggled', {\n local: state.getState('local'),\n remote: state.getState('remote'),\n });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport type { PlayerLabel } from '../state/state';\nimport { consoleLogger } from '../../utils';\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 consoleLogger.debug('[session:start] received', {\n from: command.from,\n payload: command.payload,\n local: state.getState('local'),\n remote: state.getState('remote'),\n lastStart: state.getLastStart(),\n });\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 if (state.getHistory().length > 0) {\n state.clearHistory();\n consoleLogger.debug('[session:start] cleared previous match history');\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 consoleLogger.debug('[session:start] local started', { nextStarter });\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 if (state.getHistory().length > 0) {\n state.clearHistory();\n consoleLogger.debug('[session:start] cleared previous match history');\n }\n state.setLastStart(starter);\n state.dispatch('local', 'REMOTE_START', localTarget);\n state.dispatch('remote', 'START', remoteTarget);\n consoleLogger.debug('[session:start] remote started', { starter });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport { consoleLogger, 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. Always send MOVE to peer, then let both peers check GAME_OVER locally\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 movePayload = command.payload;\n\n consoleLogger.debug('[session:move] received', {\n from: command.from,\n payload: movePayload,\n local: state.getState('local'),\n remote: state.getState('remote'),\n turn: state.getTurnCount(),\n history: state.getHistory().length,\n });\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 const message: SessionMessage = {\n type: 'MOVE',\n turn,\n payload: movePayload,\n };\n send(message);\n consoleLogger.debug('[session:move] local move sent', {\n turn,\n payload: 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 consoleLogger.debug('[session:move] game over detected', {\n winner,\n 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 consoleLogger.debug('[session:move] local game over applied', {\n winner,\n turn,\n });\n return;\n }\n\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 consoleLogger.debug('[session:move] game over detected', { winner, turn });\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 consoleLogger.debug('[session:move] remote game over applied', {\n winner,\n turn,\n });\n return;\n }\n\n consoleLogger.debug('[session:move] remote move applied', {\n turn,\n payload: movePayload,\n });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport { consoleLogger } from '../../utils';\n\ntype RequestAction = 'undo' | 'restart';\ntype RequestPayload = {\n action?: string;\n reason?: string;\n};\n\nconst isRequestAction = (value: unknown): value is RequestAction =>\n value === 'undo' || value === 'restart';\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 payload = command.payload as RequestPayload | undefined;\n const pendingAction = state.getPendingAction();\n const action =\n pendingAction ??\n (command.from === 'local' &&\n command.type === 'REJECT' &&\n isRequestAction(payload?.action)\n ? payload.action\n : null);\n consoleLogger.debug('[session:request] received', {\n type: command.type,\n from: command.from,\n action,\n local: state.getState('local'),\n remote: state.getState('remote'),\n history: state.getHistory().length,\n });\n\n if (!action) {\n console.warn('[Request] No pending action', { commandType: command.type });\n return;\n }\n\n // Verify payload matches pending action\n if (payload?.action && payload.action !== action) {\n console.warn('[Request] Action mismatch', {\n pending: action,\n payload: payload.action,\n });\n return;\n }\n\n if (command.from === 'local') {\n if (!pendingAction && command.type === 'REJECT') {\n send({\n type: 'REJECT',\n payload: { action, reason: payload?.reason ?? 'rejected' },\n });\n consoleLogger.debug('[session:request] local auto rejected', {\n action,\n reason: payload?.reason,\n });\n return;\n }\n\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 consoleLogger.debug('[session:request] local approved', { action });\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 consoleLogger.debug('[session:request] local rejected', { action });\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(\n '[Request] Cannot APPROVE from current state (remote approved)',\n );\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 consoleLogger.debug('[session:request] remote approved', { action });\n return;\n }\n\n // REJECT from remote\n if (!state.canAction('local', 'REJECT')) {\n console.warn(\n '[Request] Cannot REJECT from current state (remote rejected)',\n );\n state.clearPendingStates();\n return;\n }\n\n // Use special method for complex REJECT transition\n state.dispatchReject();\n state.clearPendingStates();\n consoleLogger.debug('[session:request] remote rejected', { action });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport type { PlayerLabel, TurnEntry } from '../state/state';\nimport { consoleLogger } from '../../utils';\n\nconst fromPeerPerspective = (player: PlayerLabel): PlayerLabel =>\n player === 'local' ? 'remote' : 'local';\n\ntype SyncPayload = {\n history?: TurnEntry[];\n lastStart?: PlayerLabel | null;\n turn?: PlayerLabel;\n resumeTurn?: PlayerLabel | null;\n};\n\nconst getCurrentTurn = (): PlayerLabel => {\n const state = getState();\n return (\n state.getResumeTurn() ??\n (state.getState('local') === 'turn' ? 'local' : 'remote')\n );\n};\n\nconst buildSyncPayload = (): SyncPayload => {\n const state = getState();\n const turn = getCurrentTurn();\n\n return {\n history: state.getHistory(),\n lastStart: state.getLastStart(),\n turn,\n resumeTurn: state.getResumeTurn(),\n };\n};\n\nconst isInSyncRecovery = (): boolean => {\n const state = getState();\n return (\n state.getState('local') === 'syncing' ||\n state.getState('remote') === 'syncing' ||\n state.getState('remote') === 'offline' ||\n state.getResumeTurn() !== null\n );\n};\n\nconst enterSyncState = (): boolean => {\n const state = getState();\n\n if (state.getState('local') !== 'syncing') {\n if (!state.canAction('local', 'SYNC')) {\n consoleLogger.debug('[session:sync] local cannot enter sync', {\n local: state.getState('local'),\n });\n return false;\n }\n state.dispatch('local', 'SYNC', 'syncing');\n }\n\n if (state.getState('remote') === 'offline') {\n if (!state.canAction('remote', 'ONLINE')) {\n consoleLogger.debug('[session:sync] offline remote cannot enter sync', {\n remote: state.getState('remote'),\n });\n return false;\n }\n state.dispatch('remote', 'ONLINE', 'syncing');\n } else if (state.getState('remote') !== 'syncing') {\n if (!state.canAction('remote', 'SYNC')) {\n consoleLogger.debug('[session:sync] remote cannot enter sync', {\n remote: state.getState('remote'),\n });\n return false;\n }\n state.dispatch('remote', 'SYNC', 'syncing');\n }\n\n return (\n state.getState('local') === 'syncing' &&\n state.getState('remote') === 'syncing'\n );\n};\n\nconst restoreFromPayload = (\n payload: SyncPayload,\n mapPeerLabels: boolean,\n): void => {\n const state = getState();\n const mapPlayer = mapPeerLabels\n ? fromPeerPerspective\n : (player: PlayerLabel) => player;\n\n if (payload.history && payload.history.length > 0) {\n state.replaceHistory(\n payload.history.map((entry) => ({\n ...entry,\n player: mapPlayer(entry.player),\n })),\n );\n } else {\n state.clearHistory();\n }\n\n if (payload.lastStart) {\n state.setLastStart(mapPlayer(payload.lastStart));\n } else {\n state.setLastStart(null);\n }\n\n const nextPlayer = payload.resumeTurn\n ? mapPlayer(payload.resumeTurn)\n : payload.turn\n ? mapPlayer(payload.turn)\n : getCurrentTurn();\n\n consoleLogger.debug('[session:sync] state restored', {\n historyLength: state.getHistory().length,\n lastStart: state.getLastStart(),\n nextTurnPlayer: nextPlayer,\n mapped: mapPeerLabels,\n });\n\n if (!enterSyncState()) {\n return;\n }\n\n state.dispatchSyncComplete(nextPlayer);\n};\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 * All player labels in SYNC_STATE are from the sender's perspective, so the\n * receiver maps local <-> remote before applying them.\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 consoleLogger.debug('[session:sync] received', {\n type: command.type,\n from: command.from,\n payload: command.payload,\n local: state.getState('local'),\n remote: state.getState('remote'),\n history: state.getHistory().length,\n resumeTurn: state.getResumeTurn(),\n });\n\n if (command.type === 'SYNC_REQUEST') {\n if (command.from === 'local') {\n // Local player initiated sync (after reconnection)\n if (\n state.getState('local') !== 'syncing' &&\n !state.canAction('local', 'SYNC')\n ) {\n console.warn('[Sync] Cannot SYNC from current state');\n return;\n }\n\n // Both transition to syncing state. Local may already be syncing when\n // OFFLINE abandoned a pending approval before ONLINE arrived.\n if (state.getState('local') !== 'syncing') {\n state.dispatch('local', 'SYNC', 'syncing');\n }\n if (state.getState('remote') !== 'syncing') {\n state.dispatch('remote', 'SYNC', 'syncing');\n }\n\n // Send request to peer (peer will respond with SYNC_STATE)\n send({ type: 'SYNC_REQUEST', from: '', payload: command.payload });\n consoleLogger.debug('[session:sync] request sent');\n return;\n }\n\n // Remote initiated sync. Send our current timeline first, then also close\n // our own FSM recovery path; the peer might be the only side requesting.\n const payload = buildSyncPayload();\n send({ type: 'SYNC_STATE', from: '', payload });\n consoleLogger.debug('[session:sync] state sent', payload);\n if (isInSyncRecovery()) {\n restoreFromPayload(payload, false);\n }\n return;\n }\n\n if (command.type !== 'SYNC_STATE') {\n return;\n }\n\n if (command.from === 'local') {\n const payload = buildSyncPayload();\n send({ type: 'SYNC_STATE', from: '', payload });\n consoleLogger.debug('[session:sync] state pushed', payload);\n if (isInSyncRecovery()) {\n restoreFromPayload(payload, false);\n }\n return;\n }\n\n restoreFromPayload((command.payload as SyncPayload) || {}, true);\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState, send } from '../context';\nimport { consoleLogger } from '../../utils';\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 * Undo always rolls back the requester's last move:\n * - requester has just moved -> undo 1 ply\n * - requester has already received a reply -> undo 2 plies\n *\n * If the request is rejected, the game resumes from the pre-request turn.\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 const bus = getBus();\n consoleLogger.debug('[session:undo] received', {\n from: command.from,\n local: state.getState('local'),\n remote: state.getState('remote'),\n pending: state.getPendingAction(),\n history: state.getHistory().length,\n });\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 // If it is our turn, our last move is two plies back. If it is the\n // peer's turn, our last move is the latest ply.\n const localState = state.getState('local');\n const undoCount = localState === 'turn' ? 2 : 1;\n const rejectResumePlayer = localState === 'turn' ? 'local' : 'remote';\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 // Store the pre-request turn for REJECT. APPROVE uses requester ownership\n // in dispatchApprove(), then applies the history rollback.\n state.initializeUndoRequest(undoCount as 1 | 2, rejectResumePlayer);\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 consoleLogger.debug('[session:undo] local requested', { undoCount });\n return;\n }\n\n // Remote player requested undo\n if (state.hasPendingAction()) {\n bus.emit('REJECT', { action: 'undo', reason: 'busy' }, 'local');\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 bus.emit('REJECT', { action: 'undo', reason: 'invalid_state' }, 'local');\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 bus.emit('REJECT', { action: 'undo', reason: 'invalid' }, 'local');\n return;\n }\n\n // Validate history is long enough\n if (state.getHistory().length < count) {\n bus.emit('REJECT', { action: 'undo', reason: 'no_history' }, 'local');\n return;\n }\n\n // Store the pre-request turn for REJECT. APPROVE uses requester ownership\n // in dispatchApprove(), then applies the history rollback.\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 consoleLogger.debug('[session:undo] remote requested', {\n count,\n resumePlayer,\n });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState, send } from '../context';\nimport { consoleLogger } from '../../utils';\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 const bus = getBus();\n consoleLogger.debug('[session:restart] received', {\n from: command.from,\n local: state.getState('local'),\n remote: state.getState('remote'),\n pending: state.getPendingAction(),\n history: state.getHistory().length,\n });\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 =\n 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 consoleLogger.debug('[session:restart] local requested', { resumePlayer });\n return;\n }\n\n // Remote player requested restart\n if (state.hasPendingAction()) {\n bus.emit('REJECT', { action: 'restart', reason: 'busy' }, 'local');\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 bus.emit('REJECT', { action: 'restart', reason: 'invalid_state' }, 'local');\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 consoleLogger.debug('[session:restart] remote requested', { resumePlayer });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState } from '../context';\nimport { consoleLogger } from '../../utils';\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 consoleLogger.debug('[session:connection] received', {\n type: command.type,\n from: command.from,\n local: state.getState('local'),\n remote: state.getState('remote'),\n pending: state.getPendingAction(),\n resumeTurn: state.getResumeTurn(),\n });\n\n if (command.type === 'OFFLINE') {\n // A disconnect makes an in-flight approval unverifiable. Model this as\n // sync recovery: local waits in syncing, remote stays offline until ONLINE.\n if (state.hasPendingAction()) {\n const resumeTurn = state.getResumeTurn();\n\n if (state.canAction('local', 'SYNC')) {\n state.dispatch('local', 'SYNC', 'syncing');\n }\n state.clearPendingStates();\n state.setResumeTurn(resumeTurn);\n }\n\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 =\n state.getResumeTurn() ??\n (state.getState('local') === 'turn' ? 'local' : 'remote');\n state.setResumeTurn(currentTurn);\n\n // Transition to offline state\n state.dispatch('remote', 'OFFLINE', 'offline');\n consoleLogger.debug('[session:connection] remote offline', { currentTurn });\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 consoleLogger.debug(\n '[session:connection] ignored online while remote is not offline',\n {\n remote: state.getState('remote'),\n },\n );\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 // We kept the authoritative timeline while the peer was away, so push our\n // state to the reconnecting peer instead of asking it for a possibly empty one.\n bus.emit('SYNC_STATE', undefined, 'local');\n consoleLogger.debug('[session:connection] remote online, sync state pushed');\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","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';\nexport type { PendingAction, PlayerLabel, TurnEntry } from './state/state';\nexport type { SessionEvent, SessionState } from './state/fsm';\n"],"mappings":";;;;;AAOA,IAAM,UACJ,CAAC,UACD,CAAC,SAAiB,SAAmB;AACnC,QAAM,QAAQ,UAAU,UAAU,QAAQ,OAAO,QAAQ,KAAK;AAC9D,MAAI,SAAS,QAAW;AAEtB,UAAM,SAAS,IAAI;AACnB;AAAA,EACF;AAEA,QAAM,OAAO;AACf;AAEK,IAAM,gBAAwB;AAAA,EACnC,OAAO,QAAQ,OAAO;AAAA,EACtB,MAAM,QAAQ,MAAM;AAAA,EACpB,MAAM,QAAQ,MAAM;AAAA,EACpB,OAAO,QAAQ,OAAO;AACxB;;;ACdO,IAAM,aAAa,CACxB,QAC2D;AAC3D,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO,IAAI,UAAU,wCAAwC;AAAA,IAC/D;AAAA,EACF;AAEA,MAAI;AACF,WAAO,EAAE,IAAI,MAAM,OAAO,KAAK,MAAM,GAAG,EAAO;AAAA,EACjD,SAAS,OAAO;AACd,WAAO,EAAE,IAAI,OAAO,MAAM;AAAA,EAC5B;AACF;;;ACCO,IAAM,sBAAsB,CACjC,SACyD;AACzD,MAAI,OAAO,SAAS,UAAU;AAC5B,QAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU;AAAA,IACd;AAAA,EACF;AACA,MAAI,CAAC,QAAQ,MAAM,CAAC,QAAQ,SAAS,OAAO,QAAQ,UAAU,UAAU;AACtE,WAAO;AAAA,EACT;AAEA,SAAO,QAAQ;AACjB;;;AC9BO,IAAM,aAAN,MAAiB;AAAA,EAAjB;AACL,wBAAQ,YAAuB,CAAC;AAChC,wBAAQ,mBAAiC,QAAQ,QAAQ;AAAA;AAAA,EAElD,KACL,MACA,SACA,OAAsB,SAChB;AACN,SAAK,SAAS,EAAE,MAAM,SAAS,KAAK,CAAC;AAAA,EACvC;AAAA,EAEO,SAAS,MAAsB,SAAgC;AACpE,SAAK,SAAS,IAAI,IAAI;AACtB,kBAAc,MAAM,4BAA4B,IAAI,EAAE;AAAA,EACxD;AAAA,EAEO,SAAS,SAA2B;AACzC,SAAK,kBAAkB,KAAK,gBAAgB,KAAK,YAAY;AAC3D,oBAAc,MAAM,0BAA0B,QAAQ,IAAI,IAAI;AAAA,QAC5D,MAAM,QAAQ;AAAA,QACd,SAAS,QAAQ;AAAA,QACjB,MAAM,QAAQ;AAAA,QACd,KAAK,QAAQ;AAAA,MACf,CAAC;AAED,YAAM,UAAU,KAAK,SAAS,QAAQ,IAAI;AAC1C,UAAI,SAAS;AACX,YAAI;AACF,gBAAM,QAAQ,OAAO;AACrB,wBAAc,MAAM,yBAAyB,QAAQ,IAAI,IAAI;AAAA,YAC3D,MAAM,QAAQ;AAAA,UAChB,CAAC;AAAA,QACH,SAAS,KAAK;AACZ,kBAAQ,MAAM,yBAAyB,QAAQ,IAAI,KAAK,GAAG;AAAA,QAC7D;AACA;AAAA,MACF;AAEA,oBAAc,MAAM,gCAAgC,QAAQ,IAAI,IAAI;AAAA,QAClE,MAAM,QAAQ;AAAA,MAChB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;;;ACtBA,IAAM,cAA4B;AAAA;AAAA,EAEhC,EAAE,MAAM,QAAQ,OAAO,SAAS,IAAI,QAAQ;AAAA,EAC5C,EAAE,MAAM,SAAS,OAAO,SAAS,IAAI,OAAO;AAAA,EAC5C,EAAE,MAAM,QAAQ,OAAO,gBAAgB,IAAI,cAAc;AAAA,EACzD,EAAE,MAAM,eAAe,OAAO,gBAAgB,IAAI,OAAO;AAAA,EACzD,EAAE,MAAM,SAAS,OAAO,UAAU,IAAI,OAAO;AAAA,EAC7C,EAAE,MAAM,eAAe,OAAO,UAAU,IAAI,OAAO;AAAA;AAAA,EAGnD,EAAE,MAAM,SAAS,OAAO,gBAAgB,IAAI,OAAO;AAAA,EACnD,EAAE,MAAM,SAAS,OAAO,gBAAgB,IAAI,cAAc;AAAA,EAC1D,EAAE,MAAM,eAAe,OAAO,SAAS,IAAI,OAAO;AAAA,EAClD,EAAE,MAAM,eAAe,OAAO,SAAS,IAAI,cAAc;AAAA;AAAA,EAGzD,EAAE,MAAM,QAAQ,OAAO,QAAQ,IAAI,cAAc;AAAA,EACjD,EAAE,MAAM,eAAe,OAAO,eAAe,IAAI,OAAO;AAAA,EACxD,EAAE,MAAM,QAAQ,OAAO,UAAU,IAAI,OAAO;AAAA,EAC5C,EAAE,MAAM,eAAe,OAAO,UAAU,IAAI,cAAc;AAAA;AAAA,EAG1D,EAAE,MAAM,QAAQ,OAAO,QAAQ,IAAI,mBAAmB;AAAA,EACtD,EAAE,MAAM,eAAe,OAAO,QAAQ,IAAI,mBAAmB;AAAA,EAC7D,EAAE,MAAM,QAAQ,OAAO,WAAW,IAAI,mBAAmB;AAAA,EACzD,EAAE,MAAM,eAAe,OAAO,WAAW,IAAI,mBAAmB;AAAA;AAAA,EAGhE,EAAE,MAAM,QAAQ,OAAO,eAAe,IAAI,YAAY;AAAA,EACtD,EAAE,MAAM,eAAe,OAAO,eAAe,IAAI,YAAY;AAAA,EAC7D,EAAE,MAAM,QAAQ,OAAO,kBAAkB,IAAI,YAAY;AAAA,EACzD,EAAE,MAAM,eAAe,OAAO,kBAAkB,IAAI,YAAY;AAAA;AAAA,EAGhE,EAAE,MAAM,oBAAoB,OAAO,WAAW,IAAI,OAAO;AAAA,EACzD,EAAE,MAAM,oBAAoB,OAAO,UAAU,IAAI,OAAO;AAAA,EACxD,EAAE,MAAM,oBAAoB,OAAO,UAAU,IAAI,cAAc;AAAA;AAAA;AAAA,EAI/D,EAAE,MAAM,aAAa,OAAO,WAAW,IAAI,cAAc;AAAA,EACzD,EAAE,MAAM,aAAa,OAAO,UAAU,IAAI,cAAc;AAAA,EACxD,EAAE,MAAM,aAAa,OAAO,UAAU,IAAI,OAAO;AAAA;AAAA,EAGjD,EAAE,MAAM,QAAQ,OAAO,aAAa,IAAI,OAAO;AAAA,EAC/C,EAAE,MAAM,eAAe,OAAO,aAAa,IAAI,OAAO;AAAA;AAAA,EAGtD,EAAE,MAAM,QAAQ,OAAO,QAAQ,IAAI,UAAU;AAAA,EAC7C,EAAE,MAAM,eAAe,OAAO,QAAQ,IAAI,UAAU;AAAA,EACpD,EAAE,MAAM,oBAAoB,OAAO,QAAQ,IAAI,UAAU;AAAA,EACzD,EAAE,MAAM,aAAa,OAAO,QAAQ,IAAI,UAAU;AAAA,EAClD,EAAE,MAAM,QAAQ,OAAO,QAAQ,IAAI,UAAU;AAAA,EAC7C,EAAE,MAAM,SAAS,OAAO,QAAQ,IAAI,UAAU;AAAA,EAC9C,EAAE,MAAM,eAAe,OAAO,QAAQ,IAAI,UAAU;AAAA,EACpD,EAAE,MAAM,WAAW,OAAO,iBAAiB,IAAI,OAAO;AAAA,EACtD,EAAE,MAAM,WAAW,OAAO,iBAAiB,IAAI,cAAc;AAAA;AAAA,EAG7D,EAAE,MAAM,QAAQ,OAAO,WAAW,IAAI,UAAU;AAAA,EAChD,EAAE,MAAM,SAAS,OAAO,WAAW,IAAI,UAAU;AAAA,EACjD,EAAE,MAAM,eAAe,OAAO,WAAW,IAAI,UAAU;AAAA,EACvD,EAAE,MAAM,QAAQ,OAAO,WAAW,IAAI,UAAU;AAAA,EAChD,EAAE,MAAM,eAAe,OAAO,WAAW,IAAI,UAAU;AAAA,EACvD,EAAE,MAAM,oBAAoB,OAAO,WAAW,IAAI,UAAU;AAAA,EAC5D,EAAE,MAAM,aAAa,OAAO,WAAW,IAAI,UAAU;AAAA,EACrD,EAAE,MAAM,WAAW,OAAO,WAAW,IAAI,UAAU;AAAA,EACnD,EAAE,MAAM,WAAW,OAAO,UAAU,IAAI,UAAU;AACpD;AAEA,IAAM,YAAY,CAChB,OACA,OACA,OACiB;AACjB,MAAI,IAAI;AACN,QACE,CAAC,CAAC,YAAY;AAAA,MACZ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,UAAU,SAAS,EAAE,OAAO;AAAA,IAC3D,GACA;AACA,aAAO;AAAA,IACT,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF,OAAO;AACL,UAAM,MAAM,YAAY,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,UAAU,KAAK;AACzE,WAAO,MAAM,IAAI,KAAK;AAAA,EACxB;AACF;AAEA,IAAM,eAAe,CACnB,OACA,QACA,OACY;AACZ,MAAI,IAAI;AACN,WAAO,CAAC,CAAC,YAAY;AAAA,MACnB,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,UAAU,UAAU,EAAE,OAAO;AAAA,IAC5D;AAAA,EACF;AACA,SAAO,CAAC,CAAC,YAAY,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,UAAU,MAAM;AACzE;AAEO,IAAM,aAAN,MAAiB;AAAA,EAGtB,YAAY,QAAsB,QAAQ;AAF1C,wBAAQ;AAGN,SAAK,QAAQ;AAAA,EACf;AAAA,EAEO,WAAyB;AAC9B,WAAO,KAAK;AAAA,EACd;AAAA,EAEO,aAAa,OAAqB,IAA4B;AACnE,WAAO,aAAa,KAAK,OAAO,OAAO,EAAE;AAAA,EAC3C;AAAA,EAEO,SAAS,QAAsB,IAAmB;AACvD,SAAK,QAAQ,UAAU,KAAK,OAAO,QAAQ,EAAE;AAAA,EAC/C;AACF;;;AC3FO,IAAM,oBAAN,MAA+C;AAAA,EACpD,eAAiC;AAC/B,WAAO,EAAE,OAAO,KAAK;AAAA,EACvB;AAAA,EACA,WAA+B;AAC7B,WAAO;AAAA,EACT;AACF;AAEO,IAAM,uBAAN,MAA2B;AAAA,EAA3B;AACL,wBAAQ,aAAiC,oBAAI,IAAI;AAAA;AAAA,EAEjD,UAAU,UAAgC;AACxC,SAAK,UAAU,IAAI,QAAQ;AAAA,EAC7B;AAAA,EAEA,YAAY,UAAgC;AAC1C,SAAK,UAAU,OAAO,QAAQ;AAAA,EAChC;AAAA,EAEA,qBAA2B;AACzB,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAS,iBAAiB;AAAA,MAC5B,SAAS,KAAK;AACZ,gBAAQ,MAAM,mBAAmB,GAAG;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,uBAA6B;AAC3B,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAS,mBAAmB;AAAA,MAC9B,SAAS,KAAK;AACZ,gBAAQ,MAAM,mBAAmB,GAAG;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAAA,EAEA,kBAAwB;AACtB,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAS,cAAc;AAAA,MACzB,SAAS,KAAK;AACZ,gBAAQ,MAAM,mBAAmB,GAAG;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AACF;AAEO,IAAM,oBAAN,MAAwB;AAAA,EAAxB;AACL,wBAAQ,aAAgC,oBAAI,IAAI;AAChD,wBAAQ,mBAA4C;AAAA;AAAA,EAEpD,UAAU,UAAqC;AAC7C,SAAK,UAAU,IAAI,QAAQ;AAC3B,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,QAAQ;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,YAAY,UAA+B;AACzC,SAAK,UAAU,OAAO,QAAQ;AAAA,EAChC;AAAA,EAEA,kBAAkB,UAAmC;AACnD,SAAK,kBAAkB;AACvB,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAS,cAAc,QAAQ;AAAA,MACjC,SAAS,KAAK;AACZ,gBAAQ,MAAM,uBAAuB,GAAG;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAAA,EAEA,gBAAgB,OAAwB;AACtC,UAAM,YAAY,KAAK,IAAI;AAC3B,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAS,YAAY,KAAK;AAAA,MAC5B,SAAS,KAAK;AACZ,gBAAQ,MAAM,uBAAuB,GAAG;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAAA,EAEA,uBAAuB,WAA0B;AAC/C,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAS,qBAAqB,SAAS;AAAA,MACzC,SAAS,KAAK;AACZ,gBAAQ,MAAM,uBAAuB,GAAG;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAAA,EAEA,YAAY,OAAqD;AAC/D,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI;AACF,iBAAS,UAAU,KAAK;AAAA,MAC1B,SAAS,KAAK;AACZ,gBAAQ,MAAM,uBAAuB,GAAG;AAAA,MAC1C;AAAA,IACF;AAAA,EACF;AAAA,EAEA,cAAwC;AACtC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,mBAA2B;AACzB,WAAO,KAAK,UAAU;AAAA,EACxB;AACF;AAEO,SAAS,uBACd,OACA,YAAqB,OACF;AACnB,SAAO;AAAA,IACL,YAAY,MAAM,SAAS,OAAO;AAAA,IAClC,aAAa,MAAM,SAAS,QAAQ;AAAA,IACpC,MAAM,MAAM,aAAa;AAAA,IACzB,SAAS,MAAM,WAAW;AAAA,IAC1B,WAAW,MAAM,aAAa;AAAA,IAC9B,eAAe,MAAM,iBAAiB;AAAA,IACtC;AAAA,EACF;AACF;AAEO,IAAM,wBAAN,MAAsD;AAAA,EAI3D,YACU,UACA,YACA,eAA8B,MAAM,OAC5C;AAHQ;AACA;AACA;AANV,wBAAQ,wBAAuB;AAC/B,wBAAQ,0BAAyB;AAAA,EAM9B;AAAA,EAEH,iBAAuB;AACrB,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,KAAK,uBAAuB,KAAK,yBAAyB,IAAK;AACnE,SAAK,uBAAuB;AAE5B,UAAM,WAAW,uBAAuB,KAAK,UAAU,KAAK,aAAa,CAAC;AAC1E,kBAAc,MAAM,qCAAqC;AAAA,MACvD,OAAO,SAAS;AAAA,MAChB,QAAQ,SAAS;AAAA,MACjB,MAAM,SAAS;AAAA,MACf,SAAS,SAAS,QAAQ;AAAA,MAC1B,SAAS,SAAS;AAAA,MAClB,WAAW,SAAS;AAAA,IACtB,CAAC;AACD,SAAK,WAAW,kBAAkB,QAAQ;AAAA,EAC5C;AAAA,EAEA,mBAAyB;AACvB,SAAK,eAAe;AAAA,EACtB;AAAA,EAEA,cAAoB;AAClB,SAAK,eAAe;AAAA,EACtB;AAAA,EAEA,UAAU,OAA2C;AACnD,SAAK,WAAW,gBAAgB,KAAkB;AAAA,EACpD;AACF;;;AC5NO,IAAM,QAAN,MAAY;AAAA,EAqBjB,YAAY,IAAmB,UAAyB;AAnBxD;AAAA,wBAAQ,SAAQ,IAAI,WAAW,MAAM;AACrC,wBAAQ,UAAS,IAAI,WAAW,MAAM;AAEtC;AAAA,wBAAiB,WAAyB;AAC1C,wBAAQ,YAA0B;AAElC;AAAA,wBAAiB,WAAuB,CAAC;AAEzC;AAAA,wBAAQ,iBAA+B;AACvC,wBAAQ,oBAAiC;AACzC,wBAAQ,cAAiC;AACzC,wBAAQ,aAAgC;AAGxC;AAAA,wBAAQ,cAA0B,IAAI,kBAAkB;AAGxD;AAAA,wBAAQ,wBAAuB,IAAI,qBAAqB;AA0OxD;AAAA;AAAA;AAAA;AAAA,wBAAQ,gBAAwB;AAvO9B,QAAI,IAAI;AACN,WAAK,UAAU;AAAA,IACjB;AACA,QAAI,UAAU;AACZ,WAAK,WAAW;AAAA,IAClB;AACA,kBAAc,MAAM,2BAA2B,EAAE,SAAS,IAAI,SAAS,CAAC;AAAA,EAC1E;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,uBAAuB,UAAgC;AAC5D,SAAK,qBAAqB,UAAU,QAAQ;AAAA,EAC9C;AAAA;AAAA,EAIO,QAAuB;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA,EAEO,cAA6B;AAClC,WAAO,KAAK;AAAA,EACd;AAAA,EAEO,YAAY,IAAY;AAC7B,SAAK,WAAW;AAChB,kBAAc,MAAM,iCAAiC,EAAE,UAAU,GAAG,CAAC;AAAA,EACvE;AAAA,EAEO,SAAS,QAAmC;AACjD,WAAO,KAAK,aAAa,MAAM,EAAE,SAAS;AAAA,EAC5C;AAAA,EAEO,eAAuB;AAC5B,WAAO,KAAK,QAAQ,SAAS;AAAA,EAC/B;AAAA,EAEO,aAA0B;AAC/B,WAAO,KAAK,QAAQ,MAAM;AAAA,EAC5B;AAAA,EAEO,eAAe,SAA4B;AAChD,kBAAc,MAAM,6BAA6B,EAAE,OAAO,QAAQ,OAAO,CAAC;AAC1E,SAAK,aAAa;AAClB,YAAQ,QAAQ,CAAC,UAAU;AACzB,WAAK,YAAY;AAAA,QACf,MAAM,MAAM;AAAA,QACZ,QAAQ,MAAM;AAAA,QACd,MAAM,MAAM;AAAA,MACd,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEO,eAAqB;AAC1B,UAAM,QAAQ,KAAK,QAAQ;AAC3B,SAAK,QAAQ,OAAO,GAAG,KAAK,QAAQ,MAAM;AAC1C,kBAAc,MAAM,2BAA2B,EAAE,MAAM,CAAC;AACxD,SAAK,qBAAqB;AAAA,EAC5B;AAAA,EAEO,YAAY,OAAwB;AACzC,SAAK,QAAQ,KAAK,KAAK;AACvB,kBAAc,MAAM,0BAA0B;AAAA,MAC5C,MAAM,MAAM;AAAA,MACZ,QAAQ,MAAM;AAAA,MACd,MAAM,MAAM;AAAA,MACZ,OAAO,KAAK,QAAQ;AAAA,IACtB,CAAC;AACD,SAAK,qBAAqB;AAAA,EAC5B;AAAA,EAEO,aAA+B;AACpC,UAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK;AACpC,QAAI,OAAO;AACT,oBAAc,MAAM,yBAAyB;AAAA,QAC3C,MAAM,MAAM;AAAA,QACZ,QAAQ,MAAM;AAAA,QACd,MAAM,MAAM;AAAA,QACZ,OAAO,KAAK,QAAQ;AAAA,MACtB,CAAC;AACD,WAAK,qBAAqB;AAAA,IAC5B;AACA,WAAO;AAAA,EACT;AAAA,EAEO,UAAU,QAAqB,QAA+B;AACnE,WAAO,KAAK,aAAa,MAAM,EAAE,aAAa,MAAM;AAAA,EACtD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASO,SACL,QACA,QACA,IACM;AACN,UAAM,SAAS,KAAK,SAAS,MAAM;AACnC,SAAK,aAAa,MAAM,EAAE,SAAS,QAAQ,EAAE;AAC7C,UAAM,QAAQ,KAAK,SAAS,MAAM;AAClC,kBAAc,MAAM,iBAAiB,MAAM,IAAI,MAAM,IAAI;AAAA,MACvD,MAAM;AAAA,MACN,IAAI;AAAA,MACJ,WAAW;AAAA,MACX,OAAO,KAAK,SAAS,OAAO;AAAA,MAC5B,QAAQ,KAAK,SAAS,QAAQ;AAAA,MAC9B,MAAM,KAAK,aAAa;AAAA,MACxB,SAAS,KAAK,QAAQ;AAAA,MACtB,SAAS,KAAK;AAAA,IAChB,CAAC;AACD,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EAEO,iBAAiB,QAAuB;AAC7C,kBAAc,MAAM,sCAAsC;AAAA,MACxD,MAAM,KAAK;AAAA,MACX,IAAI;AAAA,IACN,CAAC;AACD,SAAK,gBAAgB;AAAA,EACvB;AAAA,EAEO,mBAAkC;AACvC,WAAO,KAAK;AAAA,EACd;AAAA,EAEO,oBAAoB,OAAqB;AAC9C,kBAAc,MAAM,0CAA0C;AAAA,MAC5D,MAAM,KAAK;AAAA,MACX,IAAI;AAAA,IACN,CAAC;AACD,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EAEO,sBAAoC;AACzC,WAAO,KAAK;AAAA,EACd;AAAA,EAEO,aAAa,QAA4B;AAC9C,kBAAc,MAAM,kCAAkC;AAAA,MACpD,MAAM,KAAK;AAAA,MACX,IAAI;AAAA,IACN,CAAC;AACD,SAAK,YAAY;AAAA,EACnB;AAAA,EAEO,eAAmC;AACxC,WAAO,KAAK;AAAA,EACd;AAAA,EAEO,cAAc,QAA4B;AAC/C,kBAAc,MAAM,mCAAmC;AAAA,MACrD,MAAM,KAAK;AAAA,MACX,IAAI;AAAA,IACN,CAAC;AACD,SAAK,aAAa;AAAA,EACpB;AAAA,EAEO,gBAAoC;AACzC,WAAO,KAAK;AAAA,EACd;AAAA,EAEQ,aAAa,QAAiC;AACpD,WAAO,WAAW,UAAU,KAAK,QAAQ,KAAK;AAAA,EAChD;AAAA,EAEQ,qBAA2B;AACjC,kBAAc,MAAM,wCAAwC;AAAA,MAC1D,OAAO,KAAK,SAAS,OAAO;AAAA,MAC5B,QAAQ,KAAK,SAAS,QAAQ;AAAA,MAC9B,MAAM,KAAK,aAAa;AAAA,MACxB,SAAS,KAAK,QAAQ;AAAA,MACtB,SAAS,KAAK;AAAA,IAChB,CAAC;AACD,SAAK,qBAAqB,mBAAmB;AAAA,EAC/C;AAAA,EAEQ,uBAA6B;AACnC,kBAAc,MAAM,0CAA0C;AAAA,MAC5D,MAAM,KAAK,aAAa;AAAA,MACxB,SAAS,KAAK,QAAQ;AAAA,MACtB,SAAS,KAAK;AAAA,IAChB,CAAC;AACD,SAAK,qBAAqB,qBAAqB;AAAA,EACjD;AAAA,EAEQ,kBAAwB;AAC9B,kBAAc,MAAM,mCAAmC;AACvD,SAAK,qBAAqB,gBAAgB;AAAA,EAC5C;AAAA,EAEQ,aACN,aACA,SACA,cACA,UACM;AACN,UAAM,SAAS;AAAA,MACb,OAAO,KAAK,MAAM,SAAS;AAAA,MAC3B,QAAQ,KAAK,OAAO,SAAS;AAAA,IAC/B;AACA,SAAK,MAAM,SAAS,aAAa,OAAO;AACxC,SAAK,OAAO,SAAS,cAAc,QAAQ;AAC3C,kBAAc,MAAM,+BAA+B;AAAA,MACjD;AAAA,MACA,OAAO;AAAA,QACL,OAAO,KAAK,MAAM,SAAS;AAAA,QAC3B,QAAQ,KAAK,OAAO,SAAS;AAAA,MAC/B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,MAAM,KAAK,aAAa;AAAA,MACxB,SAAS,KAAK,QAAQ;AAAA,MACtB,SAAS,KAAK;AAAA,IAChB,CAAC;AACD,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EASO,iBAAiB,UAAyB;AAC/C,SAAK,eAAe;AACpB,kBAAc,MAAM,uCAAuC,EAAE,SAAS,CAAC;AAAA,EACzE;AAAA,EAEO,kBAA2B;AAChC,WAAO,KAAK;AAAA,EACd;AAAA,EAEO,oBAA0B;AAC/B,SAAK,eAAe;AACpB,kBAAc,MAAM,uCAAuC;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA,EAKO,mBAA4B;AACjC,WAAO,KAAK,kBAAkB;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKO,qBAA2B;AAChC,kBAAc,MAAM,0CAA0C;AAAA,MAC5D,SAAS,KAAK;AAAA,MACd,kBAAkB,KAAK;AAAA,MACvB,YAAY,KAAK;AAAA,IACnB,CAAC;AACD,SAAK,gBAAgB;AACrB,SAAK,mBAAmB;AACxB,SAAK,aAAa;AAClB,SAAK,mBAAmB;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKO,sBACL,WACA,YACM;AACN,SAAK,gBAAgB;AACrB,SAAK,mBAAmB;AACxB,SAAK,aAAa;AAClB,kBAAc,MAAM,4CAA4C;AAAA,MAC9D;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKO,yBAAyB,YAA+B;AAC7D,SAAK,gBAAgB;AACrB,SAAK,aAAa;AAClB,kBAAc,MAAM,+CAA+C;AAAA,MACjE;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKO,gBAAyB;AAC9B,WAAO,KAAK,kBAAkB;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKO,mBAA4B;AACjC,WAAO,KAAK,kBAAkB;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKO,UAAU,QAAe,GAAS;AACvC,kBAAc,MAAM,gCAAgC,EAAE,MAAM,CAAC;AAC7D,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,YAAkB;AACvB,kBAAc,MAAM,8BAA8B;AAAA,MAChD,OAAO,KAAK,SAAS,OAAO;AAAA,MAC5B,QAAQ,KAAK,SAAS,QAAQ;AAAA,MAC9B,SAAS,KAAK,QAAQ;AAAA,MACtB,WAAW,KAAK;AAAA,MAChB,SAAS,KAAK;AAAA,IAChB,CAAC;AACD,SAAK,aAAa;AAClB,SAAK,QAAQ,IAAI,WAAW,MAAM;AAClC,SAAK,SAAS,IAAI,WAAW,MAAM;AACnC,SAAK,YAAY;AACjB,SAAK,gBAAgB;AACrB,SAAK,mBAAmB;AACxB,SAAK,aAAa;AAClB,SAAK,gBAAgB;AACrB,SAAK,mBAAmB;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKO,kBAAkB,QAA2B;AAClD,SAAK,YAAY;AACjB,kBAAc,MAAM,yCAAyC,EAAE,OAAO,CAAC;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA,EAKO,cAAgC;AACrC,WAAO,KAAK,QAAQ,SAAS,IACzB,KAAK,QAAQ,KAAK,QAAQ,SAAS,CAAC,IACpC;AAAA,EACN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQO,kBAAwB;AAC7B,UAAM,aAAa,KAAK,MAAM,SAAS;AACvC,QAAI,eAAe,oBAAoB;AAErC,WAAK,aAAa,WAAW,QAAQ,WAAW,aAAa;AAAA,IAC/D,WAAW,eAAe,aAAa;AAErC,WAAK,aAAa,WAAW,eAAe,WAAW,MAAM;AAAA,IAC/D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,iBAAuB;AAC5B,UAAM,aAAa,KAAK,MAAM,SAAS;AAEvC,QAAI,eAAe,sBAAsB,eAAe,aAAa;AAEnE,YAAM,cAAc,KAAK,eAAe,UAAU,SAAS;AAC3D,YAAM,eAAe,KAAK,eAAe,UAAU,gBAAgB;AACnE,WAAK,aAAa,UAAU,aAAa,UAAU,YAAY;AAAA,IACjE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,cAAc,aAAgC;AACnD,UAAM,SAAS;AAAA,MACb,OAAO,KAAK,MAAM,SAAS;AAAA,MAC3B,QAAQ,KAAK,OAAO,SAAS;AAAA,MAC7B,WAAW,KAAK;AAAA,IAClB;AACA,QAAI,gBAAgB,SAAS;AAC3B,WAAK,MAAM,SAAS,SAAS,MAAM;AACnC,WAAK,OAAO,SAAS,SAAS,aAAa;AAC3C,WAAK,YAAY;AAAA,IACnB,OAAO;AACL,WAAK,MAAM,SAAS,SAAS,aAAa;AAC1C,WAAK,OAAO,SAAS,SAAS,MAAM;AACpC,WAAK,YAAY;AAAA,IACnB;AACA,kBAAc,MAAM,gCAAgC;AAAA,MAClD;AAAA,MACA;AAAA,MACA,OAAO;AAAA,QACL,OAAO,KAAK,MAAM,SAAS;AAAA,QAC3B,QAAQ,KAAK,OAAO,SAAS;AAAA,QAC7B,WAAW,KAAK;AAAA,MAClB;AAAA,IACF,CAAC;AACD,SAAK,mBAAmB;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,qBAAqB,YAA+B;AACzD,QAAI,eAAe,SAAS;AAC1B,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,OAAO;AACL,WAAK;AAAA,QACH;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQO,cAAc,QAA2B;AAC9C,SAAK,aAAa;AAClB,kBAAc,MAAM,oCAAoC;AAAA,MACtD,eAAe,QAAQ,OAAO,UAAU;AAAA,MACxC,YAAY,QAAQ,OAAO,OAAO;AAAA,IACpC,CAAC;AACD,QAAI,OAAO,YAAY;AACrB,aAAO,WAAW;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,gBAA6B;AAClC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQO,aAAaA,OAAiC;AACnD,UAAM,YAAY,KAAK,eAAe;AACtC,UAAM,SAAS,KAAK,WAAW,aAAaA,OAAM,SAAS;AAC3D,kBAAc,MAAM,kCAAkC;AAAA,MACpD,MAAAA;AAAA,MACA;AAAA,MACA,OAAO,UAAU;AAAA,MACjB,QAAQ,UAAU;AAAA,MAClB,MAAM,UAAU;AAAA,MAChB,SAAS,UAAU,QAAQ;AAAA,IAC7B,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOO,WAA+B;AACpC,UAAM,YAAY,KAAK,eAAe;AACtC,UAAM,SAAS,KAAK,WAAW,SAAS,WAAW,KAAK,WAAW,CAAC;AACpE,kBAAc,MAAM,8BAA8B;AAAA,MAChD;AAAA,MACA,MAAM,UAAU;AAAA,MAChB,SAAS,UAAU,QAAQ;AAAA,IAC7B,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKO,cAAoB;AACzB,QAAI,KAAK,WAAW,SAAS;AAC3B,WAAK,WAAW,QAAQ;AAAA,IAC1B;AACA,kBAAc,MAAM,+BAA+B;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,iBAA4B;AAClC,WAAO;AAAA,MACL,SAAS,KAAK,WAAW;AAAA,MACzB,YAAY,KAAK,SAAS,OAAO;AAAA,MACjC,aAAa,KAAK,SAAS,QAAQ;AAAA,MACnC,MAAM,KAAK,aAAa;AAAA,MACxB,WAAW,KAAK,aAAa;AAAA,IAC/B;AAAA,EACF;AACF;;;ACjjBO,IAAM,YAAN,MAAgB;AAAA,EAOd,YACY,QACA,KACjB,QACA;AAHiB;AACA;AARnB,wBAAQ;AACR,wBAAQ;AACR,wBAAQ,eAAuB;AAC/B,wBAAQ,4BAA2D,MAAM;AAAA,IAAC;AAC1E,wBAAQ,sBAAgD,MAAM;AAAA,IAAC;AAO7D,SAAK,cAAc,UAAU;AAC7B,SAAK,eAAe;AAGpB,SAAK,OAAO,UAAU,CAAC,SAAS;AAC9B,YAAM,UAAU,oBAAoB,IAAI;AACxC,UAAI,CAAC,WAAW,OAAO,YAAY,YAAY,CAAC,QAAQ,MAAM;AAC5D;AAAA,MACF;AACA,WAAK,IAAI,SAAS;AAAA,QAChB,GAAI;AAAA,QACJ,MAAM,QAAQ;AAAA,QACd,MAAM;AAAA,MACR,CAAC;AAAA,IACH,CAAC;AAGD,SAAK,OAAO,cAAc,CAAC,UAAU;AACnC,YAAM,eAAe,KAAK;AAC1B,WAAK,cAAc,UAAU;AAG7B,WAAK,yBAAyB,KAAK,WAAW;AAG9C,UAAI,KAAK,eAAe,CAAC,cAAc;AACrC,aAAK,IAAI,KAAK,UAAU,QAAW,OAAO;AAAA,MAC5C,WAAW,CAAC,KAAK,eAAe,cAAc;AAC5C,aAAK,IAAI,KAAK,WAAW,QAAW,OAAO;AAAA,MAC7C;AAAA,IACF,CAAC;AAGD,SAAK,OAAO,eAAe,CAAC,WAAW;AACrC,YAAM,SACJ,CAAC,CAAC,UACF,OAAO,UAAU,EAAE,KAAK,CAAC,UAAU,MAAM,eAAe,MAAM;AAChE,WAAK,mBAAmB,MAAM;AAAA,IAChC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,KAAK,SAAyB;AACnC,QAAI,CAAC,KAAK,aAAa;AACrB,cAAQ;AAAA,QACN;AAAA,QACA,QAAQ;AAAA,MACV;AACA;AAAA,IACF;AAEA,UAAM,WAA2B;AAAA,MAC/B,GAAG;AAAA,MACH,MAAM,QAAQ,QAAQ,KAAK,eAAe;AAAA,IAC5C;AACA,SAAK,OAAO,KAAK,QAAQ;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKO,WAAW,KAAwD;AACxE,QAAI,IAAI,UAAU,QAAW;AAC3B,WAAK,cAAc,IAAI;AAAA,IACzB;AACA,QAAI,IAAI,WAAW,QAAW;AAC5B,WAAK,eAAe,IAAI;AAAA,IAC1B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,aAAa;AAClB,WAAO,EAAE,OAAO,KAAK,aAAa,QAAQ,KAAK,aAAa;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA,EAKO,iBAA0B;AAC/B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,mBAAmB,SAAyC;AACjE,SAAK,2BAA2B;AAEhC,YAAQ,KAAK,WAAW;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMO,mBAAmB,SAAoC;AAC5D,SAAK,qBAAqB;AAAA,EAC5B;AACF;AAEO,IAAM,kBAAkB,CAC7B,QACA,KACA,WACG,IAAI,UAAU,QAAQ,KAAK,MAAM;;;ACxHtC,IAAM,iBAAN,MAAqB;AAAA,EAMnB,YAAY,OAAc,KAAiB,KAAgB,KAAc;AALzE,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AACjB,wBAAiB;AAGf,SAAK,QAAQ;AACb,SAAK,MAAM;AACX,SAAK,MAAM;AACX,SAAK,MAAM;AAAA,EACb;AAAA,EAEA,WAAW;AACT,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAS;AACP,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAS;AACP,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,SAAS;AACP,WAAO,KAAK;AAAA,EACd;AACF;AAEA,IAAI,WAAkC;AAO/B,IAAM,oBAAoB,CAC/B,OACA,KACA,KACA,QACG;AACH,aAAW,IAAI,eAAe,OAAO,KAAK,KAAK,GAAG;AACpD;AAeA,IAAM,iBAAiB,MAAM;AAC3B,MAAI,CAAC,UAAU;AACb,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAMO,IAAM,WAAW,MAAM,eAAe,EAAE,SAAS;AAMjD,IAAM,SAAS,MAAM,eAAe,EAAE,OAAO;AAa7C,IAAM,SAAS,MAAM,eAAe,EAAE,OAAO;AAM7C,IAAM,OAAO,CAAC,YACnB,eAAe,EAAE,OAAO,EAAE,KAAK,OAAO;;;AC/FjC,IAAM,QAAyB,CAAC,YAAY;AACjD,QAAM,QAAQ,SAAS;AACvB,QAAM,MAAM,OAAO;AACnB,QAAM,WAAW,OAAO;AACxB,gBAAc,MAAM,4BAA4B;AAAA,IAC9C,MAAM,QAAQ;AAAA,IACd,KAAM,QAAgB;AAAA,IACtB;AAAA,IACA,OAAO,MAAM,SAAS,OAAO;AAAA,IAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,EACjC,CAAC;AAED,MAAI,QAAQ,SAAS,SAAS;AAE5B,QAAI,CAAC,MAAM,UAAU,SAAS,OAAO,GAAG;AACtC,cAAQ,KAAK,oDAAoD;AAAA,QAC/D,OAAO,MAAM,SAAS,OAAO;AAAA,MAC/B,CAAC;AACD;AAAA,IACF;AAEA,UAAM,SAAS,SAAS,OAAO;AAC/B,UAAM,SAAS,UAAU,cAAc;AAEvC,UAAM,UAA0B;AAAA,MAC9B,MAAM;AAAA,MACN,KAAK;AAAA,IACP;AACA,SAAK,OAAO;AACZ,kBAAc,MAAM,iCAAiC;AAAA,MACnD,OAAO,MAAM,SAAS,OAAO;AAAA,MAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,IACjC,CAAC;AACD;AAAA,EACF;AAGA,QAAM,YAAa,QAAgB;AAGnC,MAAI,CAAC,aAAa,aAAa,WAAW;AACxC,YAAQ,KAAK,+BAA+B;AAAA,MAC1C,OAAO;AAAA,MACP,QAAQ;AAAA,IACV,CAAC;AACD,QAAI,KAAK,UAAU,EAAE,QAAQ,eAAe,GAAG,OAAO;AACtD;AAAA,EACF;AAGA,MAAI,CAAC,MAAM,UAAU,UAAU,OAAO,GAAG;AACvC,YAAQ,KAAK,iDAAiD;AAAA,MAC5D,OAAO,MAAM,SAAS,QAAQ;AAAA,IAChC,CAAC;AACD;AAAA,EACF;AAGA,QAAM,SAAS,UAAU,OAAO;AAChC,QAAM,SAAS,SAAS,cAAc;AACtC,gBAAc,MAAM,kCAAkC;AAAA,IACpD,OAAO,MAAM,SAAS,OAAO;AAAA,IAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,EACjC,CAAC;AACH;;;ACnEA,IAAM,iBAAiB,CAAC,gBAAiD;AACvE,MAAI,CAAC,aAAa;AAChB,WAAO,KAAK,OAAO,IAAI,MAAM,UAAU;AAAA,EACzC;AACA,SAAO,gBAAgB,UAAU,WAAW;AAC9C;AAOO,IAAM,QAAyB,CAAC,YAAY;AACjD,QAAM,QAAQ,SAAS;AACvB,gBAAc,MAAM,4BAA4B;AAAA,IAC9C,MAAM,QAAQ;AAAA,IACd,SAAS,QAAQ;AAAA,IACjB,OAAO,MAAM,SAAS,OAAO;AAAA,IAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,IAC/B,WAAW,MAAM,aAAa;AAAA,EAChC,CAAC;AAED,MAAI,QAAQ,SAAS,SAAS;AAE5B,QACE,CAAC,MAAM,UAAU,SAAS,OAAO,KACjC,CAAC,MAAM,UAAU,UAAU,cAAc,GACzC;AACA,cAAQ,KAAK,2CAA2C;AAAA,QACtD,YAAY,MAAM,SAAS,OAAO;AAAA,QAClC,aAAa,MAAM,SAAS,QAAQ;AAAA,MACtC,CAAC;AACD;AAAA,IACF;AAEA,UAAM,cAAc,eAAe,MAAM,aAAa,CAAC;AACvD,UAAMC,eAAc,gBAAgB,UAAU,SAAS;AACvD,UAAMC,gBAAe,gBAAgB,UAAU,gBAAgB;AAE/D,QAAI,MAAM,WAAW,EAAE,SAAS,GAAG;AACjC,YAAM,aAAa;AACnB,oBAAc,MAAM,gDAAgD;AAAA,IACtE;AACA,UAAM,aAAa,WAAW;AAC9B,UAAM,SAAS,SAAS,SAASD,YAAW;AAC5C,UAAM,SAAS,UAAU,gBAAgBC,aAAY;AAGrD,SAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS,EAAE,SAAS,gBAAgB,UAAU,WAAW,WAAW;AAAA,IACtE,CAAC;AACD,kBAAc,MAAM,iCAAiC,EAAE,YAAY,CAAC;AACpE;AAAA,EACF;AAGA,QAAM,cAAe,QAAgB,SAAS;AAE9C,MAAI,CAAC,aAAa;AAChB,YAAQ,KAAK,wCAAwC,EAAE,SAAS,QAAQ,CAAC;AACzE;AAAA,EACF;AAGA,MACE,CAAC,MAAM,UAAU,SAAS,cAAc,KACxC,CAAC,MAAM,UAAU,UAAU,OAAO,GAClC;AACA,YAAQ,KAAK,2CAA2C;AAAA,MACtD,YAAY,MAAM,SAAS,OAAO;AAAA,MAClC,aAAa,MAAM,SAAS,QAAQ;AAAA,IACtC,CAAC;AACD;AAAA,EACF;AAGA,QAAM,UAAU,gBAAgB,WAAW,WAAW;AACtD,QAAM,cAAc,YAAY,UAAU,SAAS;AACnD,QAAM,eAAe,YAAY,UAAU,gBAAgB;AAE3D,MAAI,MAAM,WAAW,EAAE,SAAS,GAAG;AACjC,UAAM,aAAa;AACnB,kBAAc,MAAM,gDAAgD;AAAA,EACtE;AACA,QAAM,aAAa,OAAO;AAC1B,QAAM,SAAS,SAAS,gBAAgB,WAAW;AACnD,QAAM,SAAS,UAAU,SAAS,YAAY;AAC9C,gBAAc,MAAM,kCAAkC,EAAE,QAAQ,CAAC;AACnE;;;AClFO,IAAM,OAAwB,CAAC,YAAY;AAChD,QAAM,QAAQ,SAAS;AACvB,QAAM,cAAc,QAAQ;AAE5B,gBAAc,MAAM,2BAA2B;AAAA,IAC7C,MAAM,QAAQ;AAAA,IACd,SAAS;AAAA,IACT,OAAO,MAAM,SAAS,OAAO;AAAA,IAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,IAC/B,MAAM,MAAM,aAAa;AAAA,IACzB,SAAS,MAAM,WAAW,EAAE;AAAA,EAC9B,CAAC;AAED,MAAI,QAAQ,SAAS,SAAS;AAE5B,QAAI,CAAC,MAAM,UAAU,SAAS,MAAM,GAAG;AACrC,cAAQ,KAAK,yCAAyC;AAAA,QACpD,OAAO,MAAM,SAAS,OAAO;AAAA,MAC/B,CAAC;AACD;AAAA,IACF;AAGA,UAAMC,cAAa,MAAM,aAAa,WAAW;AACjD,QAAI,CAACA,YAAW,OAAO;AACrB,cAAQ,KAAK,iCAAiC;AAAA,QAC5C,QAAQA,YAAW;AAAA,QACnB,MAAM;AAAA,MACR,CAAC;AAED;AAAA,IACF;AAGA,UAAM,SAAS,SAAS,MAAM;AAC9B,UAAM,SAAS,UAAU,aAAa;AAGtC,UAAMC,QAAO,MAAM,aAAa;AAChC,UAAM,YAAY;AAAA,MAChB,MAAAA;AAAA,MACA,QAAQ;AAAA,MACR,MAAM;AAAA,IACR,CAAC;AAED,UAAM,UAA0B;AAAA,MAC9B,MAAM;AAAA,MACN,MAAAA;AAAA,MACA,SAAS;AAAA,IACX;AACA,SAAK,OAAO;AACZ,kBAAc,MAAM,kCAAkC;AAAA,MACpD,MAAAA;AAAA,MACA,SAAS;AAAA,IACX,CAAC;AAGD,UAAMC,UAAS,MAAM,SAAS;AAC9B,QAAIA,SAAQ;AAEV,oBAAc,MAAM,qCAAqC;AAAA,QACvD,QAAAA;AAAA,QACA,MAAAD;AAAA,MACF,CAAC;AAGD,YAAM,SAAS,SAAS,WAAW;AACnC,YAAM,SAAS,UAAU,WAAW;AACpC,YAAM,YAAY;AAClB,oBAAc,MAAM,0CAA0C;AAAA,QAC5D,QAAAC;AAAA,QACA,MAAAD;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA;AAAA,EACF;AAGA,MAAI,CAAC,MAAM,UAAU,UAAU,MAAM,GAAG;AACtC,YAAQ,KAAK,wCAAwC;AAAA,MACnD,OAAO,MAAM,SAAS,QAAQ;AAAA,IAChC,CAAC;AACD;AAAA,EACF;AAGA,QAAM,aAAa,MAAM,aAAa,WAAW;AACjD,MAAI,CAAC,WAAW,OAAO;AACrB,YAAQ,KAAK,wCAAwC;AAAA,MACnD,QAAQ,WAAW;AAAA,MACnB,MAAM;AAAA,IACR,CAAC;AACD;AAAA,EACF;AAGA,QAAM,SAAS,UAAU,MAAM;AAC/B,QAAM,SAAS,SAAS,aAAa;AAGrC,QAAM,OAAO,MAAM,aAAa;AAChC,QAAM,YAAY;AAAA,IAChB;AAAA,IACA,QAAQ;AAAA,IACR,MAAM;AAAA,EACR,CAAC;AAGD,QAAM,SAAS,MAAM,SAAS;AAC9B,MAAI,QAAQ;AAEV,kBAAc,MAAM,qCAAqC,EAAE,QAAQ,KAAK,CAAC;AAGzE,UAAM,SAAS,SAAS,WAAW;AACnC,UAAM,SAAS,UAAU,WAAW;AACpC,UAAM,YAAY;AAClB,kBAAc,MAAM,2CAA2C;AAAA,MAC7D;AAAA,MACA;AAAA,IACF,CAAC;AACD;AAAA,EACF;AAEA,gBAAc,MAAM,sCAAsC;AAAA,IACxD;AAAA,IACA,SAAS;AAAA,EACX,CAAC;AACH;;;ACzIA,IAAM,kBAAkB,CAAC,UACvB,UAAU,UAAU,UAAU;AAazB,IAAM,UAA2B,CAAC,YAAY;AACnD,MAAI,QAAQ,SAAS,aAAa,QAAQ,SAAS,UAAU;AAC3D;AAAA,EACF;AAEA,QAAM,QAAQ,SAAS;AACvB,QAAM,UAAU,QAAQ;AACxB,QAAM,gBAAgB,MAAM,iBAAiB;AAC7C,QAAM,SACJ,kBACC,QAAQ,SAAS,WAClB,QAAQ,SAAS,YACjB,gBAAgB,SAAS,MAAM,IAC3B,QAAQ,SACR;AACN,gBAAc,MAAM,8BAA8B;AAAA,IAChD,MAAM,QAAQ;AAAA,IACd,MAAM,QAAQ;AAAA,IACd;AAAA,IACA,OAAO,MAAM,SAAS,OAAO;AAAA,IAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,IAC/B,SAAS,MAAM,WAAW,EAAE;AAAA,EAC9B,CAAC;AAED,MAAI,CAAC,QAAQ;AACX,YAAQ,KAAK,+BAA+B,EAAE,aAAa,QAAQ,KAAK,CAAC;AACzE;AAAA,EACF;AAGA,MAAI,SAAS,UAAU,QAAQ,WAAW,QAAQ;AAChD,YAAQ,KAAK,6BAA6B;AAAA,MACxC,SAAS;AAAA,MACT,SAAS,QAAQ;AAAA,IACnB,CAAC;AACD;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,SAAS;AAC5B,QAAI,CAAC,iBAAiB,QAAQ,SAAS,UAAU;AAC/C,WAAK;AAAA,QACH,MAAM;AAAA,QACN,SAAS,EAAE,QAAQ,QAAQ,SAAS,UAAU,WAAW;AAAA,MAC3D,CAAC;AACD,oBAAc,MAAM,yCAAyC;AAAA,QAC3D;AAAA,QACA,QAAQ,SAAS;AAAA,MACnB,CAAC;AACD;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,WAAW;AAE9B,UAAI,CAAC,MAAM,UAAU,SAAS,SAAS,GAAG;AACxC,gBAAQ,KAAK,6CAA6C;AAC1D;AAAA,MACF;AAGA,YAAM,gBAAgB;AAGtB,UAAI,WAAW,QAAQ;AACrB,cAAM,UAAU,MAAM,oBAAoB,KAAK,CAAC;AAAA,MAClD,WAAW,WAAW,WAAW;AAC/B,cAAM,UAAU;AAAA,MAClB;AAEA,WAAK,EAAE,MAAM,WAAW,SAAS,EAAE,OAAO,EAAE,CAAC;AAC7C,YAAM,mBAAmB;AACzB,oBAAc,MAAM,oCAAoC,EAAE,OAAO,CAAC;AAClE;AAAA,IACF;AAGA,QAAI,CAAC,MAAM,UAAU,SAAS,QAAQ,GAAG;AACvC,cAAQ,KAAK,4CAA4C;AACzD;AAAA,IACF;AAGA,UAAM,eAAe;AAErB,SAAK;AAAA,MACH,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,QAAQ,SAAS,UAAU,WAAW;AAAA,IAC3D,CAAC;AACD,UAAM,mBAAmB;AACzB,kBAAc,MAAM,oCAAoC,EAAE,OAAO,CAAC;AAClE;AAAA,EACF;AAGA,MAAI,QAAQ,SAAS,WAAW;AAE9B,QAAI,CAAC,MAAM,UAAU,SAAS,SAAS,GAAG;AACxC,cAAQ;AAAA,QACN;AAAA,MACF;AACA;AAAA,IACF;AAGA,UAAM,gBAAgB;AAGtB,QAAI,WAAW,QAAQ;AACrB,YAAM,UAAU,MAAM,oBAAoB,KAAK,CAAC;AAAA,IAClD,WAAW,WAAW,WAAW;AAC/B,YAAM,UAAU;AAAA,IAClB;AAEA,UAAM,mBAAmB;AACzB,kBAAc,MAAM,qCAAqC,EAAE,OAAO,CAAC;AACnE;AAAA,EACF;AAGA,MAAI,CAAC,MAAM,UAAU,SAAS,QAAQ,GAAG;AACvC,YAAQ;AAAA,MACN;AAAA,IACF;AACA,UAAM,mBAAmB;AACzB;AAAA,EACF;AAGA,QAAM,eAAe;AACrB,QAAM,mBAAmB;AACzB,gBAAc,MAAM,qCAAqC,EAAE,OAAO,CAAC;AACrE;;;ACrJA,IAAM,sBAAsB,CAAC,WAC3B,WAAW,UAAU,WAAW;AASlC,IAAM,iBAAiB,MAAmB;AACxC,QAAM,QAAQ,SAAS;AACvB,SACE,MAAM,cAAc,MACnB,MAAM,SAAS,OAAO,MAAM,SAAS,UAAU;AAEpD;AAEA,IAAM,mBAAmB,MAAmB;AAC1C,QAAM,QAAQ,SAAS;AACvB,QAAM,OAAO,eAAe;AAE5B,SAAO;AAAA,IACL,SAAS,MAAM,WAAW;AAAA,IAC1B,WAAW,MAAM,aAAa;AAAA,IAC9B;AAAA,IACA,YAAY,MAAM,cAAc;AAAA,EAClC;AACF;AAEA,IAAM,mBAAmB,MAAe;AACtC,QAAM,QAAQ,SAAS;AACvB,SACE,MAAM,SAAS,OAAO,MAAM,aAC5B,MAAM,SAAS,QAAQ,MAAM,aAC7B,MAAM,SAAS,QAAQ,MAAM,aAC7B,MAAM,cAAc,MAAM;AAE9B;AAEA,IAAM,iBAAiB,MAAe;AACpC,QAAM,QAAQ,SAAS;AAEvB,MAAI,MAAM,SAAS,OAAO,MAAM,WAAW;AACzC,QAAI,CAAC,MAAM,UAAU,SAAS,MAAM,GAAG;AACrC,oBAAc,MAAM,0CAA0C;AAAA,QAC5D,OAAO,MAAM,SAAS,OAAO;AAAA,MAC/B,CAAC;AACD,aAAO;AAAA,IACT;AACA,UAAM,SAAS,SAAS,QAAQ,SAAS;AAAA,EAC3C;AAEA,MAAI,MAAM,SAAS,QAAQ,MAAM,WAAW;AAC1C,QAAI,CAAC,MAAM,UAAU,UAAU,QAAQ,GAAG;AACxC,oBAAc,MAAM,mDAAmD;AAAA,QACrE,QAAQ,MAAM,SAAS,QAAQ;AAAA,MACjC,CAAC;AACD,aAAO;AAAA,IACT;AACA,UAAM,SAAS,UAAU,UAAU,SAAS;AAAA,EAC9C,WAAW,MAAM,SAAS,QAAQ,MAAM,WAAW;AACjD,QAAI,CAAC,MAAM,UAAU,UAAU,MAAM,GAAG;AACtC,oBAAc,MAAM,2CAA2C;AAAA,QAC7D,QAAQ,MAAM,SAAS,QAAQ;AAAA,MACjC,CAAC;AACD,aAAO;AAAA,IACT;AACA,UAAM,SAAS,UAAU,QAAQ,SAAS;AAAA,EAC5C;AAEA,SACE,MAAM,SAAS,OAAO,MAAM,aAC5B,MAAM,SAAS,QAAQ,MAAM;AAEjC;AAEA,IAAM,qBAAqB,CACzB,SACA,kBACS;AACT,QAAM,QAAQ,SAAS;AACvB,QAAM,YAAY,gBACd,sBACA,CAAC,WAAwB;AAE7B,MAAI,QAAQ,WAAW,QAAQ,QAAQ,SAAS,GAAG;AACjD,UAAM;AAAA,MACJ,QAAQ,QAAQ,IAAI,CAAC,WAAW;AAAA,QAC9B,GAAG;AAAA,QACH,QAAQ,UAAU,MAAM,MAAM;AAAA,MAChC,EAAE;AAAA,IACJ;AAAA,EACF,OAAO;AACL,UAAM,aAAa;AAAA,EACrB;AAEA,MAAI,QAAQ,WAAW;AACrB,UAAM,aAAa,UAAU,QAAQ,SAAS,CAAC;AAAA,EACjD,OAAO;AACL,UAAM,aAAa,IAAI;AAAA,EACzB;AAEA,QAAM,aAAa,QAAQ,aACvB,UAAU,QAAQ,UAAU,IAC5B,QAAQ,OACN,UAAU,QAAQ,IAAI,IACtB,eAAe;AAErB,gBAAc,MAAM,iCAAiC;AAAA,IACnD,eAAe,MAAM,WAAW,EAAE;AAAA,IAClC,WAAW,MAAM,aAAa;AAAA,IAC9B,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV,CAAC;AAED,MAAI,CAAC,eAAe,GAAG;AACrB;AAAA,EACF;AAEA,QAAM,qBAAqB,UAAU;AACvC;AAwBO,IAAM,OAAwB,CAAC,YAAY;AAChD,QAAM,QAAQ,SAAS;AACvB,gBAAc,MAAM,2BAA2B;AAAA,IAC7C,MAAM,QAAQ;AAAA,IACd,MAAM,QAAQ;AAAA,IACd,SAAS,QAAQ;AAAA,IACjB,OAAO,MAAM,SAAS,OAAO;AAAA,IAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,IAC/B,SAAS,MAAM,WAAW,EAAE;AAAA,IAC5B,YAAY,MAAM,cAAc;AAAA,EAClC,CAAC;AAED,MAAI,QAAQ,SAAS,gBAAgB;AACnC,QAAI,QAAQ,SAAS,SAAS;AAE5B,UACE,MAAM,SAAS,OAAO,MAAM,aAC5B,CAAC,MAAM,UAAU,SAAS,MAAM,GAChC;AACA,gBAAQ,KAAK,uCAAuC;AACpD;AAAA,MACF;AAIA,UAAI,MAAM,SAAS,OAAO,MAAM,WAAW;AACzC,cAAM,SAAS,SAAS,QAAQ,SAAS;AAAA,MAC3C;AACA,UAAI,MAAM,SAAS,QAAQ,MAAM,WAAW;AAC1C,cAAM,SAAS,UAAU,QAAQ,SAAS;AAAA,MAC5C;AAGA,WAAK,EAAE,MAAM,gBAAgB,MAAM,IAAI,SAAS,QAAQ,QAAQ,CAAC;AACjE,oBAAc,MAAM,6BAA6B;AACjD;AAAA,IACF;AAIA,UAAM,UAAU,iBAAiB;AACjC,SAAK,EAAE,MAAM,cAAc,MAAM,IAAI,QAAQ,CAAC;AAC9C,kBAAc,MAAM,6BAA6B,OAAO;AACxD,QAAI,iBAAiB,GAAG;AACtB,yBAAmB,SAAS,KAAK;AAAA,IACnC;AACA;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,cAAc;AACjC;AAAA,EACF;AAEA,MAAI,QAAQ,SAAS,SAAS;AAC5B,UAAM,UAAU,iBAAiB;AACjC,SAAK,EAAE,MAAM,cAAc,MAAM,IAAI,QAAQ,CAAC;AAC9C,kBAAc,MAAM,+BAA+B,OAAO;AAC1D,QAAI,iBAAiB,GAAG;AACtB,yBAAmB,SAAS,KAAK;AAAA,IACnC;AACA;AAAA,EACF;AAEA,qBAAoB,QAAQ,WAA2B,CAAC,GAAG,IAAI;AACjE;;;ACrMO,IAAM,OAAwB,CAAC,YAAY;AAChD,MAAI,QAAQ,SAAS,QAAQ;AAC3B;AAAA,EACF;AAEA,QAAM,QAAQ,SAAS;AACvB,QAAM,MAAM,OAAO;AACnB,gBAAc,MAAM,2BAA2B;AAAA,IAC7C,MAAM,QAAQ;AAAA,IACd,OAAO,MAAM,SAAS,OAAO;AAAA,IAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,IAC/B,SAAS,MAAM,iBAAiB;AAAA,IAChC,SAAS,MAAM,WAAW,EAAE;AAAA,EAC9B,CAAC;AAED,MAAI,QAAQ,SAAS,SAAS;AAE5B,QAAI,MAAM,iBAAiB,GAAG;AAC5B;AAAA,IACF;AAGA,QAAI,CAAC,MAAM,UAAU,SAAS,MAAM,GAAG;AACrC,cAAQ,KAAK,uCAAuC;AACpD;AAAA,IACF;AAIA,UAAM,aAAa,MAAM,SAAS,OAAO;AACzC,UAAM,YAAY,eAAe,SAAS,IAAI;AAC9C,UAAM,qBAAqB,eAAe,SAAS,UAAU;AAG7D,QAAI,MAAM,WAAW,EAAE,SAAS,WAAW;AACzC,cAAQ,KAAK,qCAAqC,EAAE,OAAO,UAAU,CAAC;AACtE;AAAA,IACF;AAIA,UAAM,sBAAsB,WAAoB,kBAAkB;AAGlE,UAAM,SAAS,SAAS,MAAM;AAC9B,UAAM,SAAS,UAAU,aAAa;AAEtC,SAAK,EAAE,MAAM,QAAQ,SAAS,EAAE,OAAO,UAAU,EAAE,CAAC;AACpD,kBAAc,MAAM,kCAAkC,EAAE,UAAU,CAAC;AACnE;AAAA,EACF;AAGA,MAAI,MAAM,iBAAiB,GAAG;AAC5B,QAAI,KAAK,UAAU,EAAE,QAAQ,QAAQ,QAAQ,OAAO,GAAG,OAAO;AAC9D;AAAA,EACF;AAGA,MAAI,CAAC,MAAM,UAAU,SAAS,aAAa,GAAG;AAC5C,YAAQ,KAAK,0CAA0C;AACvD,QAAI,KAAK,UAAU,EAAE,QAAQ,QAAQ,QAAQ,gBAAgB,GAAG,OAAO;AACvE;AAAA,EACF;AAGA,QAAM,UAAU,QAAQ;AACxB,QAAM,QAAQ,SAAS,UAAU,IAAI,IAAI;AAGzC,MAAI,SAAS,SAAS,QAAQ,UAAU,KAAK,QAAQ,UAAU,GAAG;AAChE,QAAI,KAAK,UAAU,EAAE,QAAQ,QAAQ,QAAQ,UAAU,GAAG,OAAO;AACjE;AAAA,EACF;AAGA,MAAI,MAAM,WAAW,EAAE,SAAS,OAAO;AACrC,QAAI,KAAK,UAAU,EAAE,QAAQ,QAAQ,QAAQ,aAAa,GAAG,OAAO;AACpE;AAAA,EACF;AAIA,QAAM,eAAe,MAAM,SAAS,OAAO,MAAM,SAAS,UAAU;AAGpE,QAAM,sBAAsB,OAAgB,YAAY;AAGxD,QAAM,SAAS,SAAS,aAAa;AACrC,QAAM,SAAS,UAAU,MAAM;AAC/B,gBAAc,MAAM,mCAAmC;AAAA,IACrD;AAAA,IACA;AAAA,EACF,CAAC;AACH;;;ACpGO,IAAM,UAA2B,CAAC,YAAY;AACnD,MAAI,QAAQ,SAAS,WAAW;AAC9B;AAAA,EACF;AAEA,QAAM,QAAQ,SAAS;AACvB,QAAM,MAAM,OAAO;AACnB,gBAAc,MAAM,8BAA8B;AAAA,IAChD,MAAM,QAAQ;AAAA,IACd,OAAO,MAAM,SAAS,OAAO;AAAA,IAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,IAC/B,SAAS,MAAM,iBAAiB;AAAA,IAChC,SAAS,MAAM,WAAW,EAAE;AAAA,EAC9B,CAAC;AAED,MAAI,QAAQ,SAAS,SAAS;AAE5B,QAAI,MAAM,iBAAiB,GAAG;AAC5B;AAAA,IACF;AAGA,QAAI,CAAC,MAAM,UAAU,SAAS,SAAS,GAAG;AACxC,cAAQ,KAAK,6CAA6C;AAC1D;AAAA,IACF;AAGA,UAAME,gBACJ,MAAM,SAAS,OAAO,MAAM,SAAS,UAAU;AAGjD,UAAM,yBAAyBA,aAAY;AAG3C,UAAM,SAAS,SAAS,SAAS;AACjC,UAAM,SAAS,UAAU,gBAAgB;AAEzC,SAAK,EAAE,MAAM,UAAU,CAAC;AACxB,kBAAc,MAAM,qCAAqC,EAAE,cAAAA,cAAa,CAAC;AACzE;AAAA,EACF;AAGA,MAAI,MAAM,iBAAiB,GAAG;AAC5B,QAAI,KAAK,UAAU,EAAE,QAAQ,WAAW,QAAQ,OAAO,GAAG,OAAO;AACjE;AAAA,EACF;AAGA,MAAI,CAAC,MAAM,UAAU,SAAS,gBAAgB,GAAG;AAC/C,YAAQ,KAAK,gDAAgD;AAC7D,QAAI,KAAK,UAAU,EAAE,QAAQ,WAAW,QAAQ,gBAAgB,GAAG,OAAO;AAC1E;AAAA,EACF;AAGA,QAAM,eAAe,MAAM,SAAS,OAAO,MAAM,SAAS,UAAU;AAGpE,QAAM,yBAAyB,YAAY;AAG3C,QAAM,SAAS,SAAS,gBAAgB;AACxC,QAAM,SAAS,UAAU,SAAS;AAClC,gBAAc,MAAM,sCAAsC,EAAE,aAAa,CAAC;AAC5E;;;AC/DO,IAAM,UAA2B,CAAC,YAAY;AACnD,MAAI,QAAQ,SAAS,aAAa,QAAQ,SAAS,UAAU;AAC3D;AAAA,EACF;AAEA,QAAM,QAAQ,SAAS;AACvB,QAAM,MAAM,OAAO;AACnB,gBAAc,MAAM,iCAAiC;AAAA,IACnD,MAAM,QAAQ;AAAA,IACd,MAAM,QAAQ;AAAA,IACd,OAAO,MAAM,SAAS,OAAO;AAAA,IAC7B,QAAQ,MAAM,SAAS,QAAQ;AAAA,IAC/B,SAAS,MAAM,iBAAiB;AAAA,IAChC,YAAY,MAAM,cAAc;AAAA,EAClC,CAAC;AAED,MAAI,QAAQ,SAAS,WAAW;AAG9B,QAAI,MAAM,iBAAiB,GAAG;AAC5B,YAAM,aAAa,MAAM,cAAc;AAEvC,UAAI,MAAM,UAAU,SAAS,MAAM,GAAG;AACpC,cAAM,SAAS,SAAS,QAAQ,SAAS;AAAA,MAC3C;AACA,YAAM,mBAAmB;AACzB,YAAM,cAAc,UAAU;AAAA,IAChC;AAGA,QAAI,CAAC,MAAM,UAAU,UAAU,SAAS,GAAG;AACzC,cAAQ,KAAK,2DAA2D;AACxE;AAAA,IACF;AAGA,UAAM,cACJ,MAAM,cAAc,MACnB,MAAM,SAAS,OAAO,MAAM,SAAS,UAAU;AAClD,UAAM,cAAc,WAAW;AAG/B,UAAM,SAAS,UAAU,WAAW,SAAS;AAC7C,kBAAc,MAAM,uCAAuC,EAAE,YAAY,CAAC;AAC1E;AAAA,EACF;AAKA,MAAI,MAAM,SAAS,QAAQ,MAAM,WAAW;AAC1C,kBAAc;AAAA,MACZ;AAAA,MACA;AAAA,QACE,QAAQ,MAAM,SAAS,QAAQ;AAAA,MACjC;AAAA,IACF;AACA;AAAA,EACF;AAGA,MAAI,CAAC,MAAM,UAAU,UAAU,QAAQ,GAAG;AACxC,YAAQ,KAAK,0DAA0D;AACvE;AAAA,EACF;AAGA,QAAM,SAAS,UAAU,UAAU,SAAS;AAI5C,MAAI,KAAK,cAAc,QAAW,OAAO;AACzC,gBAAc,MAAM,uDAAuD;AAC7E;;;AC9EO,IAAM,mBAAmB,CAAC,QAAoB;AACnD,MAAI,SAAS,SAAS,KAAK;AAC3B,MAAI,SAAS,SAAS,KAAK;AAC3B,MAAI,SAAS,QAAQ,IAAI;AACzB,MAAI,SAAS,QAAQ,IAAI;AACzB,MAAI,SAAS,WAAW,OAAO;AAC/B,MAAI,SAAS,gBAAgB,IAAI;AACjC,MAAI,SAAS,cAAc,IAAI;AAC/B,MAAI,SAAS,WAAW,OAAO;AAC/B,MAAI,SAAS,UAAU,OAAO;AAC9B,MAAI,SAAS,WAAW,OAAO;AAC/B,MAAI,SAAS,UAAU,OAAO;AAChC;;;ACTO,IAAM,kBAAN,MAAiD;AAAA,EACtD,YAAoB,KAAiB;AAAjB;AAAA,EAAkB;AAAA,EAEtC,QAAc;AACZ,SAAK,IAAI,KAAK,OAAO;AAAA,EACvB;AAAA,EAEA,QAAc;AACZ,SAAK,IAAI,KAAK,OAAO;AAAA,EACvB;AAAA,EAEA,KAAK,MAAqB;AACxB,SAAK,IAAI,KAAK,QAAQ,IAAI;AAAA,EAC5B;AAAA,EAEA,OAAa;AACX,SAAK,IAAI,KAAK,MAAM;AAAA,EACtB;AAAA,EAEA,UAAgB;AACd,SAAK,IAAI,KAAK,SAAS;AAAA,EACzB;AAAA,EAEA,UAAgB;AACd,SAAK,IAAI,KAAK,SAAS;AAAA,EACzB;AAAA,EAEA,SAAe;AACb,SAAK,IAAI,KAAK,QAAQ;AAAA,EACxB;AAAA,EAEA,OAAO,KAAmB;AACxB,SAAK,IAAI,KAAK,UAAU,EAAE,IAAI,CAAC;AAAA,EACjC;AACF;;;ACrBO,IAAM,gBAAgB,CAAC,eAA8B,QAAiB;AAC3E,QAAM,MAAM,IAAI,WAAW;AAC3B,QAAM,QAAQ,IAAI,MAAM,MAAM,IAAI;AAClC,QAAM,WAAW,IAAI,kBAAkB;AACvC,QAAM,MAAM,gBAAgB,eAAe,KAAK,IAAI;AAIpD,QAAM,YAAY,IAAI;AAAA,IAAsB;AAAA,IAAO;AAAA,IAAU,MAC3D,IAAI,eAAe;AAAA,EACrB;AACA,QAAM,uBAAuB,SAAS;AAEtC,oBAAkB,OAAO,KAAK,KAAK,GAAG;AACtC,mBAAiB,GAAG;AAEpB,QAAM,UAAU,IAAI,gBAAgB,GAAG;AAEvC,MAAI,mBAAmB,CAAC,gBAAgB;AACtC,aAAS,uBAAuB,WAAW;AAC3C,aAAS,kBAAkB,uBAAuB,OAAO,WAAW,CAAC;AAAA,EACvE,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,IAAI,KAAK,KAAK,GAAG;AAAA,EACzB;AACF;","names":["move","localTarget","remoteTarget","validation","turn","winner","resumePlayer"]}
|
|
1
|
+
{"version":3,"sources":["../../utils/logger.ts","../../utils/serialization/json.ts","../../utils/protocol/session.ts","../../session/commandBus.ts","../../session/state/fsm.ts","../../session/observer/index.ts","../../session/state/state.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/pendingRequest.ts","../../session/handlers/resign.ts","../../session/handlers/busRegister.ts","../../session/actions.ts","../../session/index.ts"],"sourcesContent":["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 const write = level === 'debug' ? console.info : console[level];\n if (meta !== undefined) {\n // eslint-disable-next-line no-console\n write(message, meta);\n return;\n }\n // eslint-disable-next-line no-console\n write(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 | 'REQUEST'\n | 'RESIGN'\n | 'APPROVE'\n | 'REJECT'\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 { consoleLogger, SessionMessage, SessionMessageType } from '../utils';\n\nexport type CommandOrigin = 'local' | 'remote';\nexport type BusMessageType =\n | SessionMessageType\n | 'OFFLINE'\n | 'ONLINE'\n | 'GAME_OVER';\nexport type BusMessage = Omit<SessionMessage, 'type'> & {\n type: BusMessageType;\n};\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(\n type: BusMessageType,\n payload?: unknown,\n from: CommandOrigin = 'local',\n ): void {\n this.dispatch({ type, payload, from });\n }\n\n public register(type: BusMessageType, handler: CommandListener): void {\n this.handlers[type] = handler;\n consoleLogger.debug(`[session:bus] registered ${type}`);\n }\n\n public dispatch(message: BusMessage): void {\n this.processingQueue = this.processingQueue.then(async () => {\n consoleLogger.debug(`[session:bus] dispatch ${message.type}`, {\n from: message.from,\n payload: message.payload,\n turn: message.turn,\n sid: message.sid,\n });\n\n const handler = this.handlers[message.type];\n if (handler) {\n try {\n await handler(message);\n consoleLogger.debug(`[session:bus] handled ${message.type}`, {\n from: message.from,\n });\n } catch (err) {\n console.error(`[CommandBus] Error in ${message.type}:`, err);\n }\n return;\n }\n\n consoleLogger.debug(`[session:bus] no handler for ${message.type}`, {\n from: message.from,\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 | 'REQUEST'\n | 'REMOTE_REQUEST'\n | 'APPROVE'\n | 'REJECT'\n | 'GAME_OVER'\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 // Pre-match 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 { from: 'turn', event: 'REQUEST', to: 'waiting_approval' },\n { from: 'remote_turn', event: 'REQUEST', 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 { from: 'turn', event: 'REMOTE_REQUEST', to: 'approving' },\n { from: 'remote_turn', event: 'REMOTE_REQUEST', to: 'approving' },\n\n // Approval outcomes when we were waiting for the peer.\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 the peer's request.\n // The target turn is explicit because resumeTurn decides who continues.\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 the pre-match idle state\n { from: 'turn', event: 'GAME_OVER', to: 'idle' },\n { from: 'remote_turn', event: 'GAME_OVER', to: 'idle' },\n { from: 'waiting_approval', event: 'GAME_OVER', to: 'idle' },\n { from: 'approving', event: 'GAME_OVER', to: 'idle' },\n { from: 'syncing', 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 {\n GameOutcome,\n PendingAction,\n PlayerLabel,\n TurnEntry,\n State,\n} from '../state/state';\nimport type { SessionState } from '../state/fsm';\nimport { consoleLogger } from '../../utils';\n\nexport interface GameStateSnapshot {\n localState: SessionState;\n remoteState: SessionState;\n turn: number;\n history: TurnEntry[];\n lastStart: PlayerLabel | null;\n pendingAction: PendingAction;\n outcome: GameOutcome | null;\n connected: boolean;\n}\n\nexport interface GameEvent {\n type:\n | 'READY'\n | 'START'\n | 'MOVE'\n | 'GAME_OVER'\n | 'UNDO'\n | 'RESTART'\n | 'DRAW'\n | 'RESIGN'\n | 'OFFLINE'\n | 'ONLINE'\n | 'SYNC'\n | 'ERROR';\n payload?: unknown;\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?: unknown }): 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?: unknown }): 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(\n state: State,\n connected: boolean = false,\n): 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 outcome: state.getOutcome(),\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 consoleLogger.debug('[session:observer] state snapshot', {\n local: snapshot.localState,\n remote: snapshot.remoteState,\n turn: snapshot.turn,\n history: snapshot.history.length,\n pending: snapshot.pendingAction,\n connected: snapshot.connected,\n });\n this.uiObserver.notifyStateChange(snapshot);\n }\n\n onHistoryChanged(): void {\n this.onStateChanged();\n }\n\n onGameReset(): void {\n this.onStateChanged();\n }\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';\nimport { consoleLogger } from '../../utils';\n\nexport type TurnEntry = {\n turn: number;\n player: 'local' | 'remote';\n move?: unknown;\n};\n\nexport type PlayerLabel = 'local' | 'remote';\nexport type PendingActionId = 'undo' | 'restart' | 'draw';\nexport type PendingAction = PendingActionId | null;\nexport type GameOutcome =\n | {\n kind: 'win';\n winner: PlayerLabel;\n reason: 'rules' | 'resignation';\n }\n | {\n kind: 'draw';\n reason: 'agreement' | 'mutual_resignation';\n };\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: PendingAction = null;\n private pendingUndoCount: 1 | 2 | null = null;\n private resumeTurn: PlayerLabel | null = null;\n private lastStart: PlayerLabel | null = null;\n private outcome: GameOutcome | 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 consoleLogger.debug('[session:state] created', { localId: id, remoteId });\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 consoleLogger.debug('[session:state] remote id set', { 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 consoleLogger.debug('[session:history] replace', { count: entries.length });\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 const count = this.history.length;\n this.history.splice(0, this.history.length);\n consoleLogger.debug('[session:history] clear', { count });\n this.notifyHistoryChanged();\n }\n\n public pushHistory(entry: TurnEntry): void {\n this.history.push(entry);\n consoleLogger.debug('[session:history] push', {\n turn: entry.turn,\n player: entry.player,\n move: entry.move,\n count: this.history.length,\n });\n this.notifyHistoryChanged();\n }\n\n public popHistory(): TurnEntry | null {\n const entry = this.history.pop() ?? null;\n if (entry) {\n consoleLogger.debug('[session:history] pop', {\n turn: entry.turn,\n player: entry.player,\n move: entry.move,\n count: this.history.length,\n });\n this.notifyHistoryChanged();\n }\n return entry;\n }\n\n public canAction(player: PlayerLabel, action: SessionEvent): 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 const before = this.getState(player);\n this.getPlayerFsm(player).dispatch(action, to);\n const after = this.getState(player);\n consoleLogger.debug(`[session:fsm] ${player} ${action}`, {\n from: before,\n to: after,\n requested: to,\n local: this.getState('local'),\n remote: this.getState('remote'),\n turn: this.getTurnCount(),\n history: this.history.length,\n pending: this.pendingAction,\n });\n this.notifyStateChanged();\n }\n\n public setPendingAction(action: PendingAction) {\n consoleLogger.debug('[session:state] pending action set', {\n from: this.pendingAction,\n to: action,\n });\n this.pendingAction = action;\n }\n\n public getPendingAction(): PendingAction {\n return this.pendingAction;\n }\n\n public setPendingUndoCount(count: 1 | 2 | null) {\n consoleLogger.debug('[session:state] pending undo count set', {\n from: this.pendingUndoCount,\n to: count,\n });\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 consoleLogger.debug('[session:state] last start set', {\n from: this.lastStart,\n to: player,\n });\n this.lastStart = player;\n }\n\n public getLastStart(): PlayerLabel | null {\n return this.lastStart;\n }\n\n public setResumeTurn(player: PlayerLabel | null) {\n consoleLogger.debug('[session:state] resume turn set', {\n from: this.resumeTurn,\n to: player,\n });\n this.resumeTurn = player;\n }\n\n public getResumeTurn(): PlayerLabel | null {\n return this.resumeTurn;\n }\n\n public setOutcome(outcome: GameOutcome | null): void {\n consoleLogger.debug('[session:state] outcome set', {\n from: this.outcome,\n to: outcome,\n });\n this.outcome = outcome;\n this.notifyStateChanged();\n }\n\n public getOutcome(): GameOutcome | null {\n return this.outcome;\n }\n\n private getPlayerFsm(player: PlayerLabel): SessionFsm {\n return player === 'local' ? this.local : this.remote;\n }\n\n private notifyStateChanged(): void {\n consoleLogger.debug('[session:state] notify state changed', {\n local: this.getState('local'),\n remote: this.getState('remote'),\n turn: this.getTurnCount(),\n history: this.history.length,\n pending: this.pendingAction,\n });\n this.stateObserverManager.notifyStateChanged();\n }\n\n private notifyHistoryChanged(): void {\n consoleLogger.debug('[session:state] notify history changed', {\n turn: this.getTurnCount(),\n history: this.history.length,\n pending: this.pendingAction,\n });\n this.stateObserverManager.notifyHistoryChanged();\n }\n\n private notifyGameReset(): void {\n consoleLogger.debug('[session:state] notify game reset');\n this.stateObserverManager.notifyGameReset();\n }\n\n private dispatchPair(\n localAction: SessionEvent,\n localTo: SessionState,\n remoteAction: SessionEvent,\n remoteTo: SessionState,\n ): void {\n const before = {\n local: this.local.getState(),\n remote: this.remote.getState(),\n };\n this.local.dispatch(localAction, localTo);\n this.remote.dispatch(remoteAction, remoteTo);\n consoleLogger.debug('[session:fsm] pair dispatch', {\n before,\n after: {\n local: this.local.getState(),\n remote: this.remote.getState(),\n },\n localAction,\n localTo,\n remoteAction,\n remoteTo,\n turn: this.getTurnCount(),\n history: this.history.length,\n pending: this.pendingAction,\n });\n this.notifyStateChanged();\n }\n\n // ===== Helper Methods for Approval 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 consoleLogger.debug('[session:state] game snapshot saved', { snapshot });\n }\n\n public getGameSnapshot(): unknown {\n return this.gameSnapshot;\n }\n\n public clearGameSnapshot(): void {\n this.gameSnapshot = null;\n consoleLogger.debug('[session:state] game snapshot cleared');\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 consoleLogger.debug('[session:state] pending states cleared', {\n pending: this.pendingAction,\n pendingUndoCount: this.pendingUndoCount,\n resumeTurn: this.resumeTurn,\n });\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(\n undoCount: 1 | 2,\n resumeTurn: PlayerLabel,\n ): void {\n this.pendingAction = 'undo';\n this.pendingUndoCount = undoCount;\n this.resumeTurn = resumeTurn;\n consoleLogger.debug('[session:state] undo request initialized', {\n undoCount,\n resumeTurn,\n });\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 consoleLogger.debug('[session:state] restart request initialized', {\n resumeTurn,\n });\n }\n\n public initializePendingRequest(\n action: Exclude<PendingActionId, 'undo' | 'restart'>,\n resumeTurn: PlayerLabel,\n ): void {\n this.pendingAction = action;\n this.resumeTurn = resumeTurn;\n consoleLogger.debug('[session:state] approval request initialized', {\n action,\n resumeTurn,\n });\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 consoleLogger.debug('[session:history] apply undo', { count });\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 consoleLogger.debug('[session:state] reset game', {\n local: this.getState('local'),\n remote: this.getState('remote'),\n history: this.history.length,\n lastStart: this.lastStart,\n pending: this.pendingAction,\n });\n this.clearHistory();\n this.local = new SessionFsm('idle');\n this.remote = new SessionFsm('idle');\n this.lastStart = null;\n this.outcome = 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 reconnect synchronization\n */\n public recordStartPlayer(player: PlayerLabel): void {\n this.lastStart = player;\n consoleLogger.debug('[session:state] start player recorded', { player });\n }\n\n /**\n * Get move to undo from history\n */\n public getLastMove(): TurnEntry | null {\n return this.history.length > 0\n ? this.history[this.history.length - 1]\n : 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 const before = {\n local: this.local.getState(),\n remote: this.remote.getState(),\n lastStart: this.lastStart,\n };\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 consoleLogger.debug('[session:fsm] start dispatch', {\n before,\n firstPlayer,\n after: {\n local: this.local.getState(),\n remote: this.remote.getState(),\n lastStart: this.lastStart,\n },\n });\n this.notifyStateChanged();\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.dispatchPair(\n 'SYNC_COMPLETE',\n 'turn',\n 'SYNC_COMPLETE',\n 'remote_turn',\n );\n } else {\n this.dispatchPair(\n 'SYNC_COMPLETE',\n 'remote_turn',\n 'SYNC_COMPLETE',\n 'turn',\n );\n }\n this.resumeTurn = null;\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 consoleLogger.debug('[session:plugin] game plugin set', {\n hasInitialize: Boolean(plugin.initialize),\n hasCleanup: Boolean(plugin.cleanup),\n });\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 const result = this.gamePlugin.validateMove(move, gameState);\n consoleLogger.debug('[session:plugin] validate move', {\n move,\n result,\n local: gameState.localState,\n remote: gameState.remoteState,\n turn: gameState.turn,\n history: gameState.history.length,\n });\n return result;\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 const winner = this.gamePlugin.checkWin(gameState, this.getHistory());\n consoleLogger.debug('[session:plugin] check win', {\n winner,\n turn: gameState.turn,\n history: gameState.history.length,\n });\n return winner;\n }\n\n public completeGame(outcome: GameOutcome): void {\n this.setOutcome(outcome);\n if (this.canAction('local', 'GAME_OVER')) {\n this.dispatch('local', 'GAME_OVER');\n }\n if (this.canAction('remote', 'GAME_OVER')) {\n this.dispatch('remote', 'GAME_OVER');\n }\n this.clearPendingStates();\n this.cleanupGame();\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 consoleLogger.debug('[session:plugin] cleanup game');\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","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 { consoleLogger, 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 consoleLogger.debug('[session:ready] received', {\n from: command.from,\n sid: (command as any).sid,\n localSid,\n local: state.getState('local'),\n remote: state.getState('remote'),\n });\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 consoleLogger.debug('[session:ready] local toggled', {\n local: state.getState('local'),\n remote: state.getState('remote'),\n });\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 consoleLogger.debug('[session:ready] remote toggled', {\n local: state.getState('local'),\n remote: state.getState('remote'),\n });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport type { PlayerLabel } from '../state/state';\nimport { consoleLogger } from '../../utils';\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 consoleLogger.debug('[session:start] received', {\n from: command.from,\n payload: command.payload,\n local: state.getState('local'),\n remote: state.getState('remote'),\n lastStart: state.getLastStart(),\n });\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 if (state.getHistory().length > 0) {\n state.clearHistory();\n consoleLogger.debug('[session:start] cleared previous match history');\n }\n state.setOutcome(null);\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 consoleLogger.debug('[session:start] local started', { nextStarter });\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 if (state.getHistory().length > 0) {\n state.clearHistory();\n consoleLogger.debug('[session:start] cleared previous match history');\n }\n state.setOutcome(null);\n state.setLastStart(starter);\n state.dispatch('local', 'REMOTE_START', localTarget);\n state.dispatch('remote', 'START', remoteTarget);\n consoleLogger.debug('[session:start] remote started', { starter });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport { consoleLogger, 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. Always send MOVE to peer, then let both peers check GAME_OVER locally\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 movePayload = command.payload;\n\n consoleLogger.debug('[session:move] received', {\n from: command.from,\n payload: movePayload,\n local: state.getState('local'),\n remote: state.getState('remote'),\n turn: state.getTurnCount(),\n history: state.getHistory().length,\n });\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 const message: SessionMessage = {\n type: 'MOVE',\n turn,\n payload: movePayload,\n };\n send(message);\n consoleLogger.debug('[session:move] local move sent', {\n turn,\n payload: 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 consoleLogger.debug('[session:move] game over detected', {\n winner,\n turn,\n });\n\n state.completeGame({ kind: 'win', winner, reason: 'rules' });\n consoleLogger.debug('[session:move] local game over applied', {\n winner,\n turn,\n });\n return;\n }\n\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 consoleLogger.debug('[session:move] game over detected', { winner, turn });\n\n state.completeGame({ kind: 'win', winner, reason: 'rules' });\n consoleLogger.debug('[session:move] remote game over applied', {\n winner,\n turn,\n });\n return;\n }\n\n consoleLogger.debug('[session:move] remote move applied', {\n turn,\n payload: movePayload,\n });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport { consoleLogger } from '../../utils';\n\ntype RequestAction = 'undo' | 'restart' | 'draw';\ntype RequestPayload = {\n action?: string;\n reason?: string;\n};\n\nconst isRequestAction = (value: unknown): value is RequestAction =>\n value === 'undo' || value === 'restart' || value === 'draw';\n\nconst applyApprovedAction = (action: RequestAction): void => {\n const state = getState();\n if (action === 'undo') {\n state.applyUndo(state.getPendingUndoCount() ?? 1);\n } else if (action === 'restart') {\n state.resetGame();\n } else {\n state.completeGame({ kind: 'draw', reason: 'agreement' });\n }\n};\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 payload = command.payload as RequestPayload | undefined;\n const pendingAction = state.getPendingAction();\n const action =\n pendingAction ??\n (command.from === 'local' &&\n command.type === 'REJECT' &&\n isRequestAction(payload?.action)\n ? payload.action\n : null);\n consoleLogger.debug('[session:request] received', {\n type: command.type,\n from: command.from,\n action,\n local: state.getState('local'),\n remote: state.getState('remote'),\n history: state.getHistory().length,\n });\n\n if (!action) {\n console.warn('[Request] No pending action', { commandType: command.type });\n return;\n }\n\n // Verify payload matches pending action\n if (payload?.action && payload.action !== action) {\n console.warn('[Request] Action mismatch', {\n pending: action,\n payload: payload.action,\n });\n return;\n }\n\n if (command.from === 'local') {\n if (!pendingAction && command.type === 'REJECT') {\n send({\n type: 'REJECT',\n payload: { action, reason: payload?.reason ?? 'rejected' },\n });\n consoleLogger.debug('[session:request] local auto rejected', {\n action,\n reason: payload?.reason,\n });\n return;\n }\n\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 applyApprovedAction(action);\n\n send({ type: 'APPROVE', payload: { action } });\n state.clearPendingStates();\n consoleLogger.debug('[session:request] local approved', { action });\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 consoleLogger.debug('[session:request] local rejected', { action });\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(\n '[Request] Cannot APPROVE from current state (remote approved)',\n );\n return;\n }\n\n // Use special method for complex APPROVE transition\n state.dispatchApprove();\n\n applyApprovedAction(action);\n\n state.clearPendingStates();\n consoleLogger.debug('[session:request] remote approved', { action });\n return;\n }\n\n // REJECT from remote\n if (!state.canAction('local', 'REJECT')) {\n console.warn(\n '[Request] Cannot REJECT from current state (remote rejected)',\n );\n state.clearPendingStates();\n return;\n }\n\n // Use special method for complex REJECT transition\n state.dispatchReject();\n state.clearPendingStates();\n consoleLogger.debug('[session:request] remote rejected', { action });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport type { GameOutcome, PlayerLabel, TurnEntry } from '../state/state';\nimport { consoleLogger } from '../../utils';\n\nconst fromPeerPerspective = (player: PlayerLabel): PlayerLabel =>\n player === 'local' ? 'remote' : 'local';\n\ntype SyncPayload = {\n history?: TurnEntry[];\n lastStart?: PlayerLabel | null;\n turn?: PlayerLabel;\n resumeTurn?: PlayerLabel | null;\n outcome?: GameOutcome | null;\n};\n\nconst getCurrentTurn = (): PlayerLabel => {\n const state = getState();\n return (\n state.getResumeTurn() ??\n (state.getState('local') === 'turn' ? 'local' : 'remote')\n );\n};\n\nconst buildSyncPayload = (): SyncPayload => {\n const state = getState();\n const turn = getCurrentTurn();\n\n return {\n history: state.getHistory(),\n lastStart: state.getLastStart(),\n turn,\n resumeTurn: state.getResumeTurn(),\n outcome: state.getOutcome(),\n };\n};\n\nconst isInSyncRecovery = (): boolean => {\n const state = getState();\n return (\n state.getState('local') === 'syncing' ||\n state.getState('remote') === 'syncing' ||\n state.getState('remote') === 'offline' ||\n state.getResumeTurn() !== null\n );\n};\n\nconst enterSyncState = (): boolean => {\n const state = getState();\n\n if (state.getState('local') !== 'syncing') {\n if (!state.canAction('local', 'SYNC')) {\n consoleLogger.debug('[session:sync] local cannot enter sync', {\n local: state.getState('local'),\n });\n return false;\n }\n state.dispatch('local', 'SYNC', 'syncing');\n }\n\n if (state.getState('remote') === 'offline') {\n if (!state.canAction('remote', 'ONLINE')) {\n consoleLogger.debug('[session:sync] offline remote cannot enter sync', {\n remote: state.getState('remote'),\n });\n return false;\n }\n state.dispatch('remote', 'ONLINE', 'syncing');\n } else if (state.getState('remote') !== 'syncing') {\n if (!state.canAction('remote', 'SYNC')) {\n consoleLogger.debug('[session:sync] remote cannot enter sync', {\n remote: state.getState('remote'),\n });\n return false;\n }\n state.dispatch('remote', 'SYNC', 'syncing');\n }\n\n return (\n state.getState('local') === 'syncing' &&\n state.getState('remote') === 'syncing'\n );\n};\n\nconst restoreFromPayload = (\n payload: SyncPayload,\n mapPeerLabels: boolean,\n): void => {\n const state = getState();\n const mapPlayer = mapPeerLabels\n ? fromPeerPerspective\n : (player: PlayerLabel) => player;\n\n if (payload.history && payload.history.length > 0) {\n state.replaceHistory(\n payload.history.map((entry) => ({\n ...entry,\n player: mapPlayer(entry.player),\n })),\n );\n } else {\n state.clearHistory();\n }\n\n if (payload.lastStart) {\n state.setLastStart(mapPlayer(payload.lastStart));\n } else {\n state.setLastStart(null);\n }\n\n const mappedOutcome: GameOutcome | null = payload.outcome\n ? payload.outcome.kind === 'win'\n ? { ...payload.outcome, winner: mapPlayer(payload.outcome.winner) }\n : payload.outcome\n : null;\n state.setOutcome(mappedOutcome);\n\n const nextPlayer = payload.resumeTurn\n ? mapPlayer(payload.resumeTurn)\n : payload.turn\n ? mapPlayer(payload.turn)\n : getCurrentTurn();\n\n consoleLogger.debug('[session:sync] state restored', {\n historyLength: state.getHistory().length,\n lastStart: state.getLastStart(),\n nextTurnPlayer: nextPlayer,\n mapped: mapPeerLabels,\n });\n\n if (!enterSyncState()) {\n return;\n }\n\n if (mappedOutcome) {\n state.dispatch('local', 'GAME_OVER');\n state.dispatch('remote', 'GAME_OVER');\n } else {\n state.dispatchSyncComplete(nextPlayer);\n }\n};\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 * All player labels in SYNC_STATE are from the sender's perspective, so the\n * receiver maps local <-> remote before applying them.\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 consoleLogger.debug('[session:sync] received', {\n type: command.type,\n from: command.from,\n payload: command.payload,\n local: state.getState('local'),\n remote: state.getState('remote'),\n history: state.getHistory().length,\n resumeTurn: state.getResumeTurn(),\n });\n\n if (command.type === 'SYNC_REQUEST') {\n if (command.from === 'local') {\n // Local player initiated sync (after reconnection)\n if (\n state.getState('local') !== 'syncing' &&\n !state.canAction('local', 'SYNC')\n ) {\n console.warn('[Sync] Cannot SYNC from current state');\n return;\n }\n\n // Both transition to syncing state. Local may already be syncing when\n // OFFLINE abandoned a pending approval before ONLINE arrived.\n if (state.getState('local') !== 'syncing') {\n state.dispatch('local', 'SYNC', 'syncing');\n }\n if (state.getState('remote') !== 'syncing') {\n state.dispatch('remote', 'SYNC', 'syncing');\n }\n\n // Send request to peer (peer will respond with SYNC_STATE)\n send({ type: 'SYNC_REQUEST', from: '', payload: command.payload });\n consoleLogger.debug('[session:sync] request sent');\n return;\n }\n\n // Remote initiated sync. Send our current timeline first, then also close\n // our own FSM recovery path; the peer might be the only side requesting.\n const payload = buildSyncPayload();\n send({ type: 'SYNC_STATE', from: '', payload });\n consoleLogger.debug('[session:sync] state sent', payload);\n if (isInSyncRecovery()) {\n restoreFromPayload(payload, false);\n }\n return;\n }\n\n if (command.type !== 'SYNC_STATE') {\n return;\n }\n\n if (command.from === 'local') {\n const payload = buildSyncPayload();\n send({ type: 'SYNC_STATE', from: '', payload });\n consoleLogger.debug('[session:sync] state pushed', payload);\n if (isInSyncRecovery()) {\n restoreFromPayload(payload, false);\n }\n return;\n }\n\n restoreFromPayload((command.payload as SyncPayload) || {}, true);\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState, send } from '../context';\nimport { consoleLogger } from '../../utils';\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 * Undo always rolls back the requester's last move:\n * - requester has just moved -> undo 1 ply\n * - requester has already received a reply -> undo 2 plies\n *\n * If the request is rejected, the game resumes from the pre-request turn.\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 const bus = getBus();\n consoleLogger.debug('[session:undo] received', {\n from: command.from,\n local: state.getState('local'),\n remote: state.getState('remote'),\n pending: state.getPendingAction(),\n history: state.getHistory().length,\n });\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 // If it is our turn, our last move is two plies back. If it is the\n // peer's turn, our last move is the latest ply.\n const localState = state.getState('local');\n const undoCount = localState === 'turn' ? 2 : 1;\n const rejectResumePlayer = localState === 'turn' ? 'local' : 'remote';\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 // Store the pre-request turn for REJECT. APPROVE uses requester ownership\n // in dispatchApprove(), then applies the history rollback.\n state.initializeUndoRequest(undoCount as 1 | 2, rejectResumePlayer);\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 consoleLogger.debug('[session:undo] local requested', { undoCount });\n return;\n }\n\n // Remote player requested undo\n if (state.hasPendingAction()) {\n bus.emit('REJECT', { action: 'undo', reason: 'busy' }, 'local');\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 bus.emit('REJECT', { action: 'undo', reason: 'invalid_state' }, 'local');\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 bus.emit('REJECT', { action: 'undo', reason: 'invalid' }, 'local');\n return;\n }\n\n // Validate history is long enough\n if (state.getHistory().length < count) {\n bus.emit('REJECT', { action: 'undo', reason: 'no_history' }, 'local');\n return;\n }\n\n // Store the pre-request turn for REJECT. APPROVE uses requester ownership\n // in dispatchApprove(), then applies the history rollback.\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 consoleLogger.debug('[session:undo] remote requested', {\n count,\n resumePlayer,\n });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState, send } from '../context';\nimport { consoleLogger } from '../../utils';\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 const bus = getBus();\n consoleLogger.debug('[session:restart] received', {\n from: command.from,\n local: state.getState('local'),\n remote: state.getState('remote'),\n pending: state.getPendingAction(),\n history: state.getHistory().length,\n });\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 =\n 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 consoleLogger.debug('[session:restart] local requested', { resumePlayer });\n return;\n }\n\n // Remote player requested restart\n if (state.hasPendingAction()) {\n bus.emit('REJECT', { action: 'restart', reason: 'busy' }, 'local');\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 bus.emit('REJECT', { action: 'restart', reason: 'invalid_state' }, 'local');\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 consoleLogger.debug('[session:restart] remote requested', { resumePlayer });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState } from '../context';\nimport { consoleLogger } from '../../utils';\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 consoleLogger.debug('[session:connection] received', {\n type: command.type,\n from: command.from,\n local: state.getState('local'),\n remote: state.getState('remote'),\n pending: state.getPendingAction(),\n resumeTurn: state.getResumeTurn(),\n });\n\n if (command.type === 'OFFLINE') {\n // A disconnect makes an in-flight approval unverifiable. Model this as\n // sync recovery: local waits in syncing, remote stays offline until ONLINE.\n if (state.hasPendingAction()) {\n const resumeTurn = state.getResumeTurn();\n\n if (state.canAction('local', 'SYNC')) {\n state.dispatch('local', 'SYNC', 'syncing');\n }\n state.clearPendingStates();\n state.setResumeTurn(resumeTurn);\n }\n\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 =\n state.getResumeTurn() ??\n (state.getState('local') === 'turn' ? 'local' : 'remote');\n state.setResumeTurn(currentTurn);\n\n // Transition to offline state\n state.dispatch('remote', 'OFFLINE', 'offline');\n consoleLogger.debug('[session:connection] remote offline', { currentTurn });\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 consoleLogger.debug(\n '[session:connection] ignored online while remote is not offline',\n {\n remote: state.getState('remote'),\n },\n );\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 // We kept the authoritative timeline while the peer was away, so push our\n // state to the reconnecting peer instead of asking it for a possibly empty one.\n bus.emit('SYNC_STATE', undefined, 'local');\n consoleLogger.debug('[session:connection] remote online, sync state pushed');\n};\n","import type { CommandListener } from '../commandBus';\nimport { getBus, getState, send } from '../context';\nimport type { PendingActionId, PlayerLabel } from '../state/state';\nimport { consoleLogger } from '../../utils';\n\ntype RequestPayload = { action?: unknown; payload?: unknown };\n\nconst isGenericRequestAction = (\n value: unknown,\n): value is Exclude<PendingActionId, 'undo' | 'restart'> => value === 'draw';\n\nconst currentTurn = (): PlayerLabel =>\n getState().getState('local') === 'turn' ? 'local' : 'remote';\n\nexport const pendingRequest: CommandListener = (command) => {\n if (command.type !== 'REQUEST') return;\n const state = getState();\n const bus = getBus();\n const payload = command.payload as RequestPayload | undefined;\n const action = payload?.action;\n\n if (!isGenericRequestAction(action)) {\n if (command.from === 'remote') {\n send({\n type: 'REJECT',\n payload: { action, reason: 'unknown_action' },\n });\n }\n return;\n }\n\n if (state.getOutcome()) return;\n\n if (command.from === 'local') {\n if (state.hasPendingAction() || !state.canAction('local', 'REQUEST')) {\n return;\n }\n const resumeTurn = currentTurn();\n state.initializePendingRequest(action, resumeTurn);\n state.dispatch('local', 'REQUEST');\n state.dispatch('remote', 'REMOTE_REQUEST');\n send({ type: 'REQUEST', payload: { action, payload: payload?.payload } });\n consoleLogger.debug('[session:request] local requested', { action });\n return;\n }\n\n if (state.hasPendingAction()) {\n bus.emit('REJECT', { action, reason: 'busy' }, 'local');\n return;\n }\n if (!state.canAction('local', 'REMOTE_REQUEST')) {\n bus.emit('REJECT', { action, reason: 'invalid_state' }, 'local');\n return;\n }\n\n const resumeTurn = currentTurn();\n state.initializePendingRequest(action, resumeTurn);\n state.dispatch('local', 'REMOTE_REQUEST');\n state.dispatch('remote', 'REQUEST');\n consoleLogger.debug('[session:request] remote requested', { action });\n};\n","import type { CommandListener } from '../commandBus';\nimport { getState, send } from '../context';\nimport { consoleLogger } from '../../utils';\n\nexport const resign: CommandListener = (command) => {\n if (command.type !== 'RESIGN') return;\n const state = getState();\n\n const previous = state.getOutcome();\n if (previous?.kind === 'win' && previous.reason === 'resignation') {\n state.setOutcome({ kind: 'draw', reason: 'mutual_resignation' });\n consoleLogger.debug('[session:resign] simultaneous resignation resolved');\n return;\n }\n if (previous || state.hasPendingAction()) return;\n\n const active =\n state.getState('local') === 'turn' ||\n state.getState('local') === 'remote_turn';\n if (!active) return;\n\n if (command.from === 'local') {\n send({ type: 'RESIGN' });\n state.completeGame({\n kind: 'win',\n winner: 'remote',\n reason: 'resignation',\n });\n } else {\n state.completeGame({\n kind: 'win',\n winner: 'local',\n reason: 'resignation',\n });\n }\n consoleLogger.debug('[session:resign] game completed', {\n from: command.from,\n outcome: state.getOutcome(),\n });\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';\nimport { pendingRequest } from './pendingRequest';\nimport { resign } from './resign';\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('REQUEST', pendingRequest);\n bus.register('RESIGN', resign);\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","import type { CommandBus } from './commandBus';\nimport type { PendingActionId } from './state/state';\n\nexport interface ISessionActions {\n ready(): void;\n start(): void;\n move(data: unknown): void;\n undo(): void;\n restart(): void;\n request(action: PendingActionId, payload?: unknown): void;\n offerDraw(): void;\n resign(): void;\n approve(): void;\n reject(): 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 request(action: PendingActionId, payload?: unknown): void {\n this.bus.emit('REQUEST', { action, payload });\n }\n\n offerDraw(): void {\n this.request('draw');\n }\n\n resign(): void {\n this.bus.emit('RESIGN');\n }\n\n approve(): void {\n this.bus.emit('APPROVE');\n }\n\n reject(): void {\n this.bus.emit('REJECT');\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 Protocol scope identifier shared by both peers (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';\nexport type {\n GameOutcome,\n PendingAction,\n PendingActionId,\n PlayerLabel,\n TurnEntry,\n} from './state/state';\nexport type { SessionEvent, SessionState } from './state/fsm';\n"],"mappings":"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","names":["move","localTarget","remoteTarget","validation","turn","winner","resumePlayer","currentTurn","resumeTurn"]}
|
package/package.json
CHANGED
|
@@ -34,7 +34,7 @@ class BoundaryClient {
|
|
|
34
34
|
|
|
35
35
|
const createConnectedSession = () => {
|
|
36
36
|
const client = new BoundaryClient();
|
|
37
|
-
const session = createSession(client, 'demo-
|
|
37
|
+
const session = createSession(client, 'demo-session');
|
|
38
38
|
|
|
39
39
|
session.net.setPeerIds({ local: 'local', remote: 'remote' });
|
|
40
40
|
client.connect();
|
|
@@ -44,7 +44,7 @@ const createConnectedSession = () => {
|
|
|
44
44
|
|
|
45
45
|
const startGame = async () => {
|
|
46
46
|
const runtime = createConnectedSession();
|
|
47
|
-
runtime.client.inbound({ type: 'READY', sid: 'demo-
|
|
47
|
+
runtime.client.inbound({ type: 'READY', sid: 'demo-session', from: 'remote' });
|
|
48
48
|
await waitForBus();
|
|
49
49
|
runtime.session.actions.start();
|
|
50
50
|
await waitForBus();
|
|
@@ -60,7 +60,7 @@ const startGame = async () => {
|
|
|
60
60
|
|
|
61
61
|
const startGameWithFirstPlayer = async (firstPlayer) => {
|
|
62
62
|
const runtime = createConnectedSession();
|
|
63
|
-
runtime.client.inbound({ type: 'READY', sid: 'demo-
|
|
63
|
+
runtime.client.inbound({ type: 'READY', sid: 'demo-session', from: 'remote' });
|
|
64
64
|
await waitForBus();
|
|
65
65
|
runtime.session.state.setLastStart(
|
|
66
66
|
firstPlayer === 'local' ? 'remote' : 'local',
|
|
@@ -99,7 +99,7 @@ const oneMoveWinPlugin = {
|
|
|
99
99
|
const sent = client.sent.at(-1);
|
|
100
100
|
assert.equal(typeof sent, 'object');
|
|
101
101
|
assert.equal(sent.type, 'READY');
|
|
102
|
-
assert.equal(sent.sid, 'demo-
|
|
102
|
+
assert.equal(sent.sid, 'demo-session');
|
|
103
103
|
assert.equal(session.state.getState('local'), 'ready');
|
|
104
104
|
assert.equal(session.state.getState('remote'), 'could_start');
|
|
105
105
|
}
|
|
@@ -108,7 +108,7 @@ const oneMoveWinPlugin = {
|
|
|
108
108
|
const { client, session } = createConnectedSession();
|
|
109
109
|
await waitForBus();
|
|
110
110
|
|
|
111
|
-
client.inbound({ type: 'READY', sid: 'demo-
|
|
111
|
+
client.inbound({ type: 'READY', sid: 'demo-session', from: 'remote' });
|
|
112
112
|
await waitForBus();
|
|
113
113
|
|
|
114
114
|
assert.equal(session.state.getState('local'), 'could_start');
|
|
@@ -120,7 +120,7 @@ const oneMoveWinPlugin = {
|
|
|
120
120
|
await waitForBus();
|
|
121
121
|
|
|
122
122
|
client.inbound(
|
|
123
|
-
JSON.stringify({ type: 'READY', sid: 'demo-
|
|
123
|
+
JSON.stringify({ type: 'READY', sid: 'demo-session', from: 'remote' }),
|
|
124
124
|
);
|
|
125
125
|
await waitForBus();
|
|
126
126
|
|
|
@@ -238,7 +238,7 @@ const oneMoveWinPlugin = {
|
|
|
238
238
|
assert.equal(session.state.getState('local'), 'idle');
|
|
239
239
|
assert.equal(session.state.getState('remote'), 'idle');
|
|
240
240
|
|
|
241
|
-
client.inbound({ type: 'READY', sid: 'demo-
|
|
241
|
+
client.inbound({ type: 'READY', sid: 'demo-session', from: 'remote' });
|
|
242
242
|
await waitForBus();
|
|
243
243
|
session.actions.start();
|
|
244
244
|
await waitForBus();
|
|
@@ -569,4 +569,115 @@ const oneMoveWinPlugin = {
|
|
|
569
569
|
assert.equal(session.state.getHistory()[0].player, 'local');
|
|
570
570
|
}
|
|
571
571
|
|
|
572
|
+
{
|
|
573
|
+
const { client, session } = await startGameWithFirstPlayer('local');
|
|
574
|
+
session.actions.move({ step: 'keep-this-move' });
|
|
575
|
+
await waitForBus();
|
|
576
|
+
|
|
577
|
+
session.actions.offerDraw();
|
|
578
|
+
await waitForBus();
|
|
579
|
+
|
|
580
|
+
const sent = client.sent.at(-1);
|
|
581
|
+
assert.equal(sent.type, 'REQUEST');
|
|
582
|
+
assert.equal(sent.payload.action, 'draw');
|
|
583
|
+
assert.equal(session.state.getPendingAction(), 'draw');
|
|
584
|
+
assert.equal(session.state.getState('local'), 'waiting_approval');
|
|
585
|
+
assert.equal(session.state.getState('remote'), 'approving');
|
|
586
|
+
|
|
587
|
+
client.inbound({
|
|
588
|
+
type: 'APPROVE',
|
|
589
|
+
payload: { action: 'draw' },
|
|
590
|
+
from: 'remote',
|
|
591
|
+
});
|
|
592
|
+
await waitForBus();
|
|
593
|
+
|
|
594
|
+
assert.deepEqual(session.state.getOutcome(), {
|
|
595
|
+
kind: 'draw',
|
|
596
|
+
reason: 'agreement',
|
|
597
|
+
});
|
|
598
|
+
assert.equal(session.state.getHistory().length, 1);
|
|
599
|
+
assert.equal(session.state.getState('local'), 'idle');
|
|
600
|
+
assert.equal(session.state.getState('remote'), 'idle');
|
|
601
|
+
assert.equal(session.observer.getSnapshot().outcome.kind, 'draw');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
{
|
|
605
|
+
const { client, session } = await startGame();
|
|
606
|
+
const before = {
|
|
607
|
+
local: session.state.getState('local'),
|
|
608
|
+
remote: session.state.getState('remote'),
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
client.inbound({
|
|
612
|
+
type: 'REQUEST',
|
|
613
|
+
payload: { action: 'draw' },
|
|
614
|
+
from: 'remote',
|
|
615
|
+
});
|
|
616
|
+
await waitForBus();
|
|
617
|
+
assert.equal(session.state.getPendingAction(), 'draw');
|
|
618
|
+
assert.equal(session.state.getState('local'), 'approving');
|
|
619
|
+
|
|
620
|
+
session.actions.reject();
|
|
621
|
+
await waitForBus();
|
|
622
|
+
assert.equal(client.sent.at(-1).payload.action, 'draw');
|
|
623
|
+
assert.equal(session.state.getOutcome(), null);
|
|
624
|
+
assert.equal(session.state.getState('local'), before.local);
|
|
625
|
+
assert.equal(session.state.getState('remote'), before.remote);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
{
|
|
629
|
+
const { client, session } = await startGame();
|
|
630
|
+
session.actions.resign();
|
|
631
|
+
await waitForBus();
|
|
632
|
+
|
|
633
|
+
assert.equal(client.sent.at(-1).type, 'RESIGN');
|
|
634
|
+
assert.deepEqual(session.state.getOutcome(), {
|
|
635
|
+
kind: 'win',
|
|
636
|
+
winner: 'remote',
|
|
637
|
+
reason: 'resignation',
|
|
638
|
+
});
|
|
639
|
+
assert.equal(session.state.getState('local'), 'idle');
|
|
640
|
+
assert.equal(session.state.getState('remote'), 'idle');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
{
|
|
644
|
+
const { client, session } = await startGame();
|
|
645
|
+
client.inbound({ type: 'RESIGN', from: 'remote' });
|
|
646
|
+
await waitForBus();
|
|
647
|
+
|
|
648
|
+
assert.deepEqual(session.state.getOutcome(), {
|
|
649
|
+
kind: 'win',
|
|
650
|
+
winner: 'local',
|
|
651
|
+
reason: 'resignation',
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
{
|
|
656
|
+
const { client, session } = createConnectedSession();
|
|
657
|
+
await waitForBus();
|
|
658
|
+
client.inbound({
|
|
659
|
+
type: 'SYNC_STATE',
|
|
660
|
+
payload: {
|
|
661
|
+
history: [],
|
|
662
|
+
lastStart: 'local',
|
|
663
|
+
turn: 'local',
|
|
664
|
+
outcome: {
|
|
665
|
+
kind: 'win',
|
|
666
|
+
winner: 'local',
|
|
667
|
+
reason: 'resignation',
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
from: 'remote',
|
|
671
|
+
});
|
|
672
|
+
await waitForBus();
|
|
673
|
+
|
|
674
|
+
assert.deepEqual(session.state.getOutcome(), {
|
|
675
|
+
kind: 'win',
|
|
676
|
+
winner: 'remote',
|
|
677
|
+
reason: 'resignation',
|
|
678
|
+
});
|
|
679
|
+
assert.equal(session.state.getState('local'), 'idle');
|
|
680
|
+
assert.equal(session.state.getState('remote'), 'idle');
|
|
681
|
+
}
|
|
682
|
+
|
|
572
683
|
console.log('serialization smoke passed');
|