@aprovan/patchwork-image-boardgameio 0.1.0

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/index.js ADDED
@@ -0,0 +1,1392 @@
1
+ // src/context.ts
2
+ var getReact = () => {
3
+ const win = window;
4
+ if (!win.React) {
5
+ throw new Error(
6
+ "[boardgameio] React not found on window. Ensure React is preloaded."
7
+ );
8
+ }
9
+ return win.React;
10
+ };
11
+ var SettingsContext = null;
12
+ function getSettingsContext() {
13
+ if (!SettingsContext) {
14
+ SettingsContext = getReact().createContext({});
15
+ }
16
+ return SettingsContext;
17
+ }
18
+ function SettingsProvider({
19
+ settings,
20
+ children
21
+ }) {
22
+ const React = getReact();
23
+ const Context = getSettingsContext();
24
+ return React.createElement(
25
+ Context.Provider,
26
+ { value: settings },
27
+ children
28
+ );
29
+ }
30
+ function useSettings() {
31
+ const React = getReact();
32
+ const Context = getSettingsContext();
33
+ return React.useContext(Context);
34
+ }
35
+
36
+ // src/p2p/authentication.ts
37
+ function generateCredentials() {
38
+ const bytes = new Uint8Array(64);
39
+ crypto.getRandomValues(bytes);
40
+ return btoa(String.fromCharCode(...bytes));
41
+ }
42
+ function generateKeyPair() {
43
+ const privateKey = new Uint8Array(64);
44
+ crypto.getRandomValues(privateKey);
45
+ const publicKey = privateKey.slice(0, 32);
46
+ return {
47
+ publicKey: btoa(String.fromCharCode(...publicKey)),
48
+ privateKey: btoa(String.fromCharCode(...privateKey))
49
+ };
50
+ }
51
+ function signMessage(message, privateKey) {
52
+ const msgBytes = new TextEncoder().encode(message);
53
+ const keyBytes = Uint8Array.from(atob(privateKey), (c) => c.charCodeAt(0));
54
+ const signature = new Uint8Array(msgBytes.length + 64);
55
+ signature.set(msgBytes);
56
+ signature.set(keyBytes.slice(0, 64), msgBytes.length);
57
+ return btoa(String.fromCharCode(...signature));
58
+ }
59
+ function verifyMessage(signedMessage, publicKey, playerID) {
60
+ try {
61
+ const sigBytes = Uint8Array.from(
62
+ atob(signedMessage),
63
+ (c) => c.charCodeAt(0)
64
+ );
65
+ const keyBytes = Uint8Array.from(atob(publicKey), (c) => c.charCodeAt(0));
66
+ if (sigBytes.length < 64 || keyBytes.length < 32) return false;
67
+ const msgBytes = sigBytes.slice(0, -64);
68
+ const decoded = new TextDecoder().decode(msgBytes);
69
+ return decoded === playerID;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ function authenticate(matchID, clientMetadata, db) {
75
+ const { playerID, credentials, message } = clientMetadata;
76
+ const { metadata } = db.fetch(matchID);
77
+ if (!metadata) return false;
78
+ if (playerID === null || playerID === void 0 || !(+playerID in metadata.players)) {
79
+ return true;
80
+ }
81
+ const existingCredentials = metadata.players[+playerID]?.credentials;
82
+ const isMessageValid = credentials ? !!message && verifyMessage(message, credentials, playerID) : false;
83
+ if (!existingCredentials && isMessageValid) {
84
+ db.setMetadata(matchID, {
85
+ ...metadata,
86
+ players: {
87
+ ...metadata.players,
88
+ [+playerID]: { ...metadata.players[+playerID], credentials }
89
+ }
90
+ });
91
+ return true;
92
+ }
93
+ if (!existingCredentials && !credentials) return true;
94
+ if (existingCredentials === credentials && isMessageValid) return true;
95
+ return false;
96
+ }
97
+
98
+ // src/p2p/db.ts
99
+ var P2PDB = class {
100
+ constructor() {
101
+ this.initialState = /* @__PURE__ */ new Map();
102
+ this.state = /* @__PURE__ */ new Map();
103
+ this.log = /* @__PURE__ */ new Map();
104
+ this.metadata = /* @__PURE__ */ new Map();
105
+ }
106
+ connect() {
107
+ }
108
+ createMatch(matchID, opts) {
109
+ this.initialState.set(matchID, opts.initialState);
110
+ this.state.set(matchID, opts.initialState);
111
+ this.log.set(matchID, opts.initialState._stateID === 0 ? [] : []);
112
+ this.metadata.set(matchID, opts.metadata);
113
+ }
114
+ setState(matchID, state, deltalog) {
115
+ this.state.set(matchID, state);
116
+ if (deltalog) {
117
+ const existing = this.log.get(matchID) ?? [];
118
+ this.log.set(matchID, [...existing, ...deltalog]);
119
+ }
120
+ }
121
+ setMetadata(matchID, metadata) {
122
+ this.metadata.set(matchID, metadata);
123
+ }
124
+ fetch(matchID) {
125
+ return {
126
+ state: this.state.get(matchID),
127
+ initialState: this.initialState.get(matchID),
128
+ log: this.log.get(matchID),
129
+ metadata: this.metadata.get(matchID)
130
+ };
131
+ }
132
+ wipe(matchID) {
133
+ this.initialState.delete(matchID);
134
+ this.state.delete(matchID);
135
+ this.log.delete(matchID);
136
+ this.metadata.delete(matchID);
137
+ }
138
+ listMatches() {
139
+ return [...this.metadata.keys()];
140
+ }
141
+ };
142
+
143
+ // src/p2p/session-storage.ts
144
+ var STORAGE_PREFIX = "p2p-session:";
145
+ var SESSION_TTL_MS = 5 * 60 * 1e3;
146
+ function getStorageKey(matchID) {
147
+ return `${STORAGE_PREFIX}${matchID}`;
148
+ }
149
+ function hasValidSession(matchID) {
150
+ const session = loadSession(matchID);
151
+ return session !== null;
152
+ }
153
+ function loadSession(matchID) {
154
+ try {
155
+ const key = getStorageKey(matchID);
156
+ const raw = localStorage.getItem(key);
157
+ if (!raw) return null;
158
+ const session = JSON.parse(raw);
159
+ const now = Date.now();
160
+ if (now - session.updatedAt > SESSION_TTL_MS) {
161
+ console.log("[P2PSessionStorage] Session expired, removing:", matchID);
162
+ removeSession(matchID);
163
+ return null;
164
+ }
165
+ console.log("[P2PSessionStorage] Loaded session:", {
166
+ matchID,
167
+ stateID: session.state._stateID,
168
+ age: Math.round((now - session.updatedAt) / 1e3) + "s"
169
+ });
170
+ return session;
171
+ } catch (err) {
172
+ console.error("[P2PSessionStorage] Failed to load session:", err);
173
+ return null;
174
+ }
175
+ }
176
+ function saveSession(session) {
177
+ try {
178
+ const key = getStorageKey(session.matchID);
179
+ session.updatedAt = Date.now();
180
+ const serialized = JSON.stringify(session);
181
+ localStorage.setItem(key, serialized);
182
+ console.log("[P2PSessionStorage] Saved session:", {
183
+ matchID: session.matchID,
184
+ stateID: session.state._stateID,
185
+ gameName: session.gameName,
186
+ sizeBytes: serialized.length
187
+ });
188
+ } catch (err) {
189
+ console.error("[P2PSessionStorage] Failed to save session:", err);
190
+ if (err instanceof DOMException && err.name === "QuotaExceededError") {
191
+ console.warn("[P2PSessionStorage] Storage quota exceeded, cleaning up old sessions");
192
+ cleanupExpiredSessions();
193
+ }
194
+ }
195
+ }
196
+ function updateSessionState(matchID, state, log) {
197
+ const session = loadSession(matchID);
198
+ if (!session) return;
199
+ session.state = state;
200
+ if (log) session.log = log;
201
+ saveSession(session);
202
+ }
203
+ function removeSession(matchID) {
204
+ try {
205
+ const key = getStorageKey(matchID);
206
+ localStorage.removeItem(key);
207
+ console.log("[P2PSessionStorage] Removed session:", matchID);
208
+ } catch (err) {
209
+ console.error("[P2PSessionStorage] Failed to remove session:", err);
210
+ }
211
+ }
212
+ function cleanupExpiredSessions() {
213
+ try {
214
+ const now = Date.now();
215
+ const keysToRemove = [];
216
+ for (let i = 0; i < localStorage.length; i++) {
217
+ const key = localStorage.key(i);
218
+ if (!key?.startsWith(STORAGE_PREFIX)) continue;
219
+ try {
220
+ const raw = localStorage.getItem(key);
221
+ if (!raw) continue;
222
+ const session = JSON.parse(raw);
223
+ if (now - session.updatedAt > SESSION_TTL_MS) {
224
+ keysToRemove.push(key);
225
+ }
226
+ } catch {
227
+ keysToRemove.push(key);
228
+ }
229
+ }
230
+ for (const key of keysToRemove) {
231
+ localStorage.removeItem(key);
232
+ console.log("[P2PSessionStorage] Cleaned up expired session:", key);
233
+ }
234
+ } catch (err) {
235
+ console.error("[P2PSessionStorage] Cleanup failed:", err);
236
+ }
237
+ }
238
+
239
+ // src/p2p/host.ts
240
+ var P2PHost = class {
241
+ constructor({
242
+ game,
243
+ numPlayers = 2,
244
+ matchID
245
+ }) {
246
+ this.clients = /* @__PURE__ */ new Map();
247
+ this.hostClient = null;
248
+ this.matchID = matchID;
249
+ this.game = game;
250
+ this.numPlayers = numPlayers;
251
+ this.db = new P2PDB();
252
+ const gameName = game.name ?? "unknown";
253
+ console.log("[P2PHost] Checking for existing session:", { matchID, gameName });
254
+ const existingSession = loadSession(matchID);
255
+ if (existingSession) {
256
+ console.log("[P2PHost] Found existing session:", {
257
+ matchID,
258
+ sessionGameName: existingSession.gameName,
259
+ expectedGameName: gameName,
260
+ stateID: existingSession.state._stateID
261
+ });
262
+ if (existingSession.gameName === gameName) {
263
+ console.log("[P2PHost] Restoring session from storage");
264
+ this.state = existingSession.state;
265
+ this.db.createMatch(matchID, {
266
+ initialState: existingSession.initialState,
267
+ metadata: existingSession.metadata
268
+ });
269
+ this.db.setState(matchID, existingSession.state, existingSession.log);
270
+ saveSession(existingSession);
271
+ return;
272
+ } else {
273
+ console.log("[P2PHost] Game name mismatch, creating new session");
274
+ }
275
+ } else {
276
+ console.log("[P2PHost] No existing session found, creating new");
277
+ }
278
+ const initialState = this.createInitialState();
279
+ this.state = initialState;
280
+ const players = {};
281
+ for (let i = 0; i < numPlayers; i++) {
282
+ players[i] = { id: i };
283
+ }
284
+ const metadata = {
285
+ gameName: game.name ?? "unknown",
286
+ players,
287
+ createdAt: Date.now(),
288
+ updatedAt: Date.now()
289
+ };
290
+ this.db.createMatch(matchID, {
291
+ initialState,
292
+ metadata
293
+ });
294
+ this.persistSession(initialState, [], metadata);
295
+ }
296
+ persistSession(state, log, metadata) {
297
+ const existingSession = loadSession(this.matchID);
298
+ const session = {
299
+ matchID: this.matchID,
300
+ gameName: this.game.name ?? "unknown",
301
+ numPlayers: this.numPlayers,
302
+ state,
303
+ initialState: existingSession?.initialState ?? state,
304
+ log,
305
+ metadata: metadata ?? existingSession?.metadata ?? {
306
+ gameName: this.game.name ?? "unknown",
307
+ players: {},
308
+ createdAt: Date.now(),
309
+ updatedAt: Date.now()
310
+ },
311
+ createdAt: existingSession?.createdAt ?? Date.now(),
312
+ updatedAt: Date.now()
313
+ };
314
+ saveSession(session);
315
+ }
316
+ createInitialState() {
317
+ const activePlayers = {};
318
+ for (let i = 0; i < this.numPlayers; i++) {
319
+ activePlayers[String(i)] = "";
320
+ }
321
+ const ctx = {
322
+ numPlayers: this.numPlayers,
323
+ turn: 1,
324
+ currentPlayer: "0",
325
+ playOrder: Array.from({ length: this.numPlayers }, (_, i) => String(i)),
326
+ playOrderPos: 0,
327
+ phase: "",
328
+ activePlayers
329
+ };
330
+ const G = this.game.setup?.({ ctx, ...ctx }) ?? {};
331
+ return {
332
+ G,
333
+ ctx,
334
+ plugins: {},
335
+ _stateID: 0,
336
+ _undo: [],
337
+ _redo: []
338
+ };
339
+ }
340
+ registerClient(client) {
341
+ if (!authenticate(this.matchID, client.metadata, this.db)) {
342
+ console.log(
343
+ "[P2PHost] Client auth failed for playerID:",
344
+ client.metadata.playerID
345
+ );
346
+ return;
347
+ }
348
+ console.log(
349
+ "[P2PHost] Registered client for playerID:",
350
+ client.metadata.playerID
351
+ );
352
+ this.clients.set(client, client);
353
+ this.syncClient(client);
354
+ }
355
+ registerHostClient(client) {
356
+ console.log(
357
+ "[P2PHost] Registered host client for playerID:",
358
+ client.metadata.playerID
359
+ );
360
+ this.hostClient = client;
361
+ this.syncClient(client);
362
+ }
363
+ unregisterClient(client) {
364
+ this.clients.delete(client);
365
+ }
366
+ processAction(client, data) {
367
+ switch (data.type) {
368
+ case "sync":
369
+ this.syncClient(client);
370
+ break;
371
+ case "update":
372
+ this.handleUpdate(data.args);
373
+ break;
374
+ case "chat":
375
+ this.broadcastChat(data.args);
376
+ break;
377
+ }
378
+ }
379
+ syncClient(client) {
380
+ const { state, log } = this.db.fetch(this.matchID);
381
+ const playerID = client.metadata.playerID;
382
+ const filteredState = this.filterStateForPlayer(state, playerID);
383
+ client.send({
384
+ type: "sync",
385
+ args: [this.matchID, { state: filteredState, log }]
386
+ });
387
+ }
388
+ filterStateForPlayer(state, _playerID) {
389
+ return state;
390
+ }
391
+ handleUpdate([matchID, , action]) {
392
+ if (matchID !== this.matchID) return;
393
+ const currentState = this.state;
394
+ if (!currentState) return;
395
+ const moveName = action.payload?.type;
396
+ const moveArgs = action.payload?.args ?? [];
397
+ const playerID = action.payload?.playerID;
398
+ const ctxCurrentPlayer = currentState.ctx.currentPlayer;
399
+ const gCurrent = currentState.G?.current;
400
+ const currentPlayer = gCurrent !== void 0 ? String(gCurrent) : ctxCurrentPlayer;
401
+ console.log("[P2PHost] handleUpdate:", { moveName, moveArgs, playerID, currentPlayer });
402
+ if (playerID !== currentPlayer) {
403
+ console.log("[P2PHost] Rejected move: wrong player", { playerID, currentPlayer });
404
+ return;
405
+ }
406
+ if (moveName && this.game.moves?.[moveName]) {
407
+ const move = this.game.moves[moveName];
408
+ const ctx = { ...currentState.ctx, playerID };
409
+ const G = JSON.parse(JSON.stringify(currentState.G));
410
+ console.log(
411
+ "[P2PHost] Before move, G.players:",
412
+ G.players?.map((p) => ({
413
+ id: p.id,
414
+ bet: p.bet
415
+ }))
416
+ );
417
+ if (typeof move === "function") {
418
+ move({ G, ctx }, ...moveArgs);
419
+ }
420
+ console.log(
421
+ "[P2PHost] After move, G.players:",
422
+ G.players?.map((p) => ({
423
+ id: p.id,
424
+ bet: p.bet
425
+ }))
426
+ );
427
+ const newState = {
428
+ ...currentState,
429
+ G,
430
+ _stateID: currentState._stateID + 1
431
+ };
432
+ this.state = newState;
433
+ this.db.setState(this.matchID, newState);
434
+ const { log } = this.db.fetch(this.matchID);
435
+ updateSessionState(this.matchID, newState, log);
436
+ console.log(
437
+ "[P2PHost] State updated, broadcasting to",
438
+ this.clients.size,
439
+ "clients"
440
+ );
441
+ this.broadcastState();
442
+ } else {
443
+ console.log(
444
+ "[P2PHost] Move not found:",
445
+ moveName,
446
+ "Available:",
447
+ Object.keys(this.game.moves || {})
448
+ );
449
+ }
450
+ }
451
+ broadcastState() {
452
+ const { state, log } = this.db.fetch(this.matchID);
453
+ if (this.hostClient) {
454
+ const playerID = this.hostClient.metadata.playerID;
455
+ const filteredState = this.filterStateForPlayer(state, playerID);
456
+ this.hostClient.send({
457
+ type: "sync",
458
+ args: [this.matchID, { state: filteredState, log }]
459
+ });
460
+ }
461
+ for (const client of this.clients.values()) {
462
+ const playerID = client.metadata.playerID;
463
+ const filteredState = this.filterStateForPlayer(state, playerID);
464
+ client.send({
465
+ type: "sync",
466
+ args: [this.matchID, { state: filteredState, log }]
467
+ });
468
+ }
469
+ }
470
+ broadcastChat(args) {
471
+ if (this.hostClient) {
472
+ this.hostClient.send({ type: "chat", args });
473
+ }
474
+ for (const client of this.clients.values()) {
475
+ client.send({ type: "chat", args });
476
+ }
477
+ }
478
+ };
479
+
480
+ // src/p2p/transport.ts
481
+ var DEFAULT_ICE_SERVERS = [
482
+ { urls: "stun:stun.l.google.com:19302" },
483
+ { urls: "stun:stun1.l.google.com:19302" },
484
+ { urls: "stun:stun2.l.google.com:19302" },
485
+ // Free TURN servers from Open Relay Project
486
+ {
487
+ urls: "turn:openrelay.metered.ca:80",
488
+ username: "openrelayproject",
489
+ credential: "openrelayproject"
490
+ },
491
+ {
492
+ urls: "turn:openrelay.metered.ca:443",
493
+ username: "openrelayproject",
494
+ credential: "openrelayproject"
495
+ },
496
+ {
497
+ urls: "turn:openrelay.metered.ca:443?transport=tcp",
498
+ username: "openrelayproject",
499
+ credential: "openrelayproject"
500
+ }
501
+ ];
502
+ var P2PTransport = class {
503
+ constructor(config, opts = {}) {
504
+ this.peer = null;
505
+ this.connection = null;
506
+ this.host = null;
507
+ this.connected = false;
508
+ this.connectionStatusCallbacks = /* @__PURE__ */ new Set();
509
+ this.hostRetryCount = 0;
510
+ this.maxHostRetries = 3;
511
+ this.lastPeerConfig = null;
512
+ this.retryCount = 0;
513
+ this.maxRetries = 6;
514
+ // Increased from 3 to handle host reconnection
515
+ this.retryDelayMs = 3e3;
516
+ console.log("[P2PTransport] Constructor called with config:", {
517
+ gameName: config.gameName,
518
+ playerID: config.playerID,
519
+ matchID: config.matchID,
520
+ numPlayers: config.numPlayers,
521
+ credentials: config.credentials ? "(present)" : "(none)"
522
+ });
523
+ console.log("[P2PTransport] Options:", {
524
+ isHost: opts.isHost
525
+ });
526
+ this.gameName = config.gameName;
527
+ this.playerID = config.playerID;
528
+ this.matchID = config.matchID;
529
+ this.numPlayers = config.numPlayers;
530
+ this.game = config.game;
531
+ this.transportDataCallback = config.transportDataCallback;
532
+ this.isHost = opts.isHost ?? false;
533
+ this.peerOptions = opts.peerOptions;
534
+ this.onError = opts.onError;
535
+ this.setCredentials(config.credentials);
536
+ }
537
+ get hostID() {
538
+ return `boardgameio-${this.gameName}-matchid-${this.matchID}`;
539
+ }
540
+ setCredentials(credentials) {
541
+ if (!credentials) {
542
+ const { publicKey, privateKey } = generateKeyPair();
543
+ this.credentials = publicKey;
544
+ this.privateKey = privateKey;
545
+ } else {
546
+ this.credentials = credentials;
547
+ }
548
+ }
549
+ get metadata() {
550
+ return {
551
+ playerID: this.playerID,
552
+ credentials: this.credentials,
553
+ message: this.privateKey && this.playerID ? signMessage(this.playerID, this.privateKey) : void 0
554
+ };
555
+ }
556
+ connect() {
557
+ cleanupExpiredSessions();
558
+ const Peer = window.Peer;
559
+ if (!Peer) {
560
+ this.onError?.(new Error("PeerJS not loaded"));
561
+ return;
562
+ }
563
+ const globalPeerConfig = window.__peerConfig ?? {};
564
+ const iceServers = globalPeerConfig.iceServers && globalPeerConfig.iceServers.length > 0 ? globalPeerConfig.iceServers : DEFAULT_ICE_SERVERS;
565
+ const baseConfig = {
566
+ host: globalPeerConfig.host,
567
+ port: globalPeerConfig.port,
568
+ path: globalPeerConfig.path,
569
+ secure: globalPeerConfig.secure,
570
+ debug: 2,
571
+ // Warnings and errors
572
+ config: {
573
+ iceServers
574
+ }
575
+ };
576
+ const peerConfig = {
577
+ ...baseConfig,
578
+ ...this.peerOptions,
579
+ config: {
580
+ ...baseConfig.config,
581
+ ...this.peerOptions && this.peerOptions.config ? this.peerOptions.config : {}
582
+ }
583
+ };
584
+ const isReconnection = this.isHost && hasValidSession(this.matchID);
585
+ console.log(
586
+ `[P2PTransport] Connecting as ${this.isHost ? "HOST" : "CLIENT"}, hostID: ${this.hostID}${isReconnection ? " (reconnecting)" : ""}`
587
+ );
588
+ if (isReconnection) {
589
+ console.log("[P2PTransport] Host reconnecting, waiting for PeerJS ID to clear...");
590
+ setTimeout(() => this.createPeer(Peer, peerConfig), 2e3);
591
+ } else {
592
+ this.createPeer(Peer, peerConfig);
593
+ }
594
+ }
595
+ createPeer(Peer, peerConfig) {
596
+ this.lastPeerConfig = peerConfig;
597
+ this.peer = new Peer(this.isHost ? this.hostID : void 0, peerConfig);
598
+ this.peer.on("open", (id) => {
599
+ console.log(`[P2PTransport] Peer opened with ID: ${id}`);
600
+ if (this.isHost) {
601
+ this.host = new P2PHost({
602
+ game: this.game,
603
+ numPlayers: this.numPlayers,
604
+ matchID: this.matchID
605
+ });
606
+ this.host.registerHostClient({
607
+ metadata: this.metadata,
608
+ send: (data) => this.notifyClient(data)
609
+ });
610
+ console.log("[P2PTransport] Host ready, waiting for connections");
611
+ this.onConnect();
612
+ } else {
613
+ console.log(`[P2PTransport] Client connecting to host: ${this.hostID}`);
614
+ this.connectToHost();
615
+ }
616
+ });
617
+ this.peer.on("connection", (conn) => {
618
+ console.log(
619
+ "[P2PTransport] Incoming connection from:",
620
+ conn.peer
621
+ );
622
+ const dataConn = conn;
623
+ if (!this.host) return;
624
+ const client = {
625
+ metadata: { playerID: null },
626
+ send: (data) => dataConn.send(data)
627
+ };
628
+ dataConn.on("open", () => {
629
+ console.log("[P2PTransport] Data connection opened");
630
+ this.host?.registerClient(client);
631
+ });
632
+ dataConn.on("data", (data) => {
633
+ const action = data;
634
+ if (action.type === "sync" && "metadata" in data) {
635
+ client.metadata = data.metadata;
636
+ }
637
+ this.host?.processAction(client, action);
638
+ });
639
+ dataConn.on("close", () => {
640
+ this.host?.unregisterClient(client);
641
+ });
642
+ });
643
+ this.peer.on("error", (err) => {
644
+ const error = err;
645
+ console.error("[P2PTransport] Peer error:", error.type, error.message);
646
+ if (error.type === "peer-unavailable") {
647
+ console.error(
648
+ "[P2PTransport] Host peer not found. Is the host connected?"
649
+ );
650
+ this.onError?.(error);
651
+ } else if (error.type === "unavailable-id") {
652
+ if (this.isHost && this.hostRetryCount < this.maxHostRetries) {
653
+ this.hostRetryCount++;
654
+ const delay = 2e3 * this.hostRetryCount;
655
+ console.log(
656
+ `[P2PTransport] Peer ID unavailable, retrying in ${delay}ms (${this.hostRetryCount}/${this.maxHostRetries})...`
657
+ );
658
+ this.peer?.destroy();
659
+ this.peer = null;
660
+ setTimeout(() => {
661
+ const Peer2 = window.Peer;
662
+ if (Peer2 && this.lastPeerConfig) {
663
+ this.createPeer(Peer2, this.lastPeerConfig);
664
+ }
665
+ }, delay);
666
+ } else {
667
+ console.error("[P2PTransport] Peer ID already taken, max retries reached");
668
+ this.onError?.(error);
669
+ }
670
+ } else {
671
+ this.onError?.(error);
672
+ }
673
+ });
674
+ this.peer.on("close", () => {
675
+ console.log("[P2PTransport] Peer connection closed");
676
+ this.connected = false;
677
+ this.notifyConnectionStatus(false);
678
+ });
679
+ }
680
+ // Delay between retries
681
+ connectToHost() {
682
+ if (!this.peer) return;
683
+ console.log(
684
+ `[P2PTransport] Attempting connection to host (attempt ${this.retryCount + 1}/${this.maxRetries + 1})`
685
+ );
686
+ this.connection = this.peer.connect(this.hostID, {
687
+ reliable: true,
688
+ serialization: "json"
689
+ });
690
+ const connectionTimeout = setTimeout(() => {
691
+ if (!this.connected && this.connection) {
692
+ this.retryCount++;
693
+ if (this.retryCount <= this.maxRetries) {
694
+ console.warn(
695
+ `[P2PTransport] Connection timeout, retrying in ${this.retryDelayMs}ms (${this.retryCount}/${this.maxRetries})...`
696
+ );
697
+ this.connection.close();
698
+ setTimeout(() => this.connectToHost(), this.retryDelayMs);
699
+ } else {
700
+ console.error(
701
+ "[P2PTransport] Max retries reached. Could not connect to host."
702
+ );
703
+ this.onError?.(
704
+ new Error(
705
+ "Could not connect to host after multiple attempts. Is the host online?"
706
+ )
707
+ );
708
+ }
709
+ }
710
+ }, 1e4);
711
+ this.connection.on("open", () => {
712
+ clearTimeout(connectionTimeout);
713
+ console.log("[P2PTransport] Connected to host successfully!");
714
+ this.retryCount = 0;
715
+ this.connection?.send({ type: "sync", metadata: this.metadata });
716
+ this.onConnect();
717
+ });
718
+ this.connection.on("data", (data) => {
719
+ this.notifyClient(data);
720
+ });
721
+ this.connection.on("close", () => {
722
+ clearTimeout(connectionTimeout);
723
+ console.log("[P2PTransport] Connection to host closed");
724
+ this.connected = false;
725
+ this.notifyConnectionStatus(false);
726
+ });
727
+ this.connection.on("error", (err) => {
728
+ clearTimeout(connectionTimeout);
729
+ const error = err;
730
+ console.error(
731
+ "[P2PTransport] Connection error:",
732
+ error.type,
733
+ error.message
734
+ );
735
+ this.onError?.(error);
736
+ });
737
+ }
738
+ onConnect() {
739
+ this.connected = true;
740
+ this.notifyConnectionStatus(true);
741
+ this.requestSync();
742
+ }
743
+ notifyConnectionStatus(connected) {
744
+ for (const callback of this.connectionStatusCallbacks) {
745
+ callback(connected);
746
+ }
747
+ }
748
+ notifyClient(data) {
749
+ this.transportDataCallback(data);
750
+ }
751
+ disconnect() {
752
+ this.connection?.close();
753
+ this.peer?.destroy();
754
+ this.peer = null;
755
+ this.connection = null;
756
+ this.host = null;
757
+ this.connected = false;
758
+ this.notifyConnectionStatus(false);
759
+ }
760
+ requestSync() {
761
+ if (this.isHost && this.host) {
762
+ this.host.processAction(
763
+ {
764
+ metadata: this.metadata,
765
+ send: (d) => this.notifyClient(d)
766
+ },
767
+ { type: "sync" }
768
+ );
769
+ } else {
770
+ this.connection?.send({ type: "sync", metadata: this.metadata });
771
+ }
772
+ }
773
+ sendAction(state, action) {
774
+ const msg = {
775
+ type: "update",
776
+ args: [this.matchID, state, action]
777
+ };
778
+ if (this.isHost && this.host) {
779
+ this.host.processAction(
780
+ {
781
+ metadata: this.metadata,
782
+ send: (d) => this.notifyClient(d)
783
+ },
784
+ msg
785
+ );
786
+ } else {
787
+ this.connection?.send(msg);
788
+ }
789
+ }
790
+ sendChatMessage(matchID, chatMessage) {
791
+ const msg = {
792
+ type: "chat",
793
+ args: [matchID, chatMessage, this.credentials]
794
+ };
795
+ if (this.isHost && this.host) {
796
+ this.host.processAction(
797
+ {
798
+ metadata: this.metadata,
799
+ send: (d) => this.notifyClient(d)
800
+ },
801
+ msg
802
+ );
803
+ } else {
804
+ this.connection?.send(msg);
805
+ }
806
+ }
807
+ updateMatchID(id) {
808
+ this.matchID = id;
809
+ this.disconnect();
810
+ this.connect();
811
+ }
812
+ updatePlayerID(id) {
813
+ this.playerID = id;
814
+ this.disconnect();
815
+ this.connect();
816
+ }
817
+ updateCredentials(credentials) {
818
+ this.setCredentials(credentials);
819
+ this.disconnect();
820
+ this.connect();
821
+ }
822
+ isConnected() {
823
+ return this.connected;
824
+ }
825
+ subscribeToConnectionStatus(callback) {
826
+ this.connectionStatusCallbacks.add(callback);
827
+ callback(this.connected);
828
+ return () => {
829
+ this.connectionStatusCallbacks.delete(callback);
830
+ };
831
+ }
832
+ };
833
+ function createP2PTransport(opts = {}) {
834
+ return (config) => new P2PTransport(config, opts);
835
+ }
836
+
837
+ // src/bot-manager.ts
838
+ var DIFFICULTY_PRESETS = {
839
+ easy: {
840
+ strategy: "random",
841
+ mctsIterations: 0,
842
+ delay: 400,
843
+ minDelay: 200,
844
+ mistakeRate: 0.3
845
+ },
846
+ medium: {
847
+ strategy: "mcts",
848
+ mctsIterations: 500,
849
+ delay: 800,
850
+ minDelay: 400,
851
+ mistakeRate: 0.1
852
+ },
853
+ hard: {
854
+ strategy: "mcts",
855
+ mctsIterations: 2e3,
856
+ delay: 1200,
857
+ minDelay: 600,
858
+ mistakeRate: 0
859
+ },
860
+ custom: {
861
+ strategy: "mcts",
862
+ mctsIterations: 1e3,
863
+ delay: 800,
864
+ minDelay: 0,
865
+ mistakeRate: 0
866
+ }
867
+ };
868
+ function resolveBotConfig(inputs, gameAiDifficulty) {
869
+ const difficulty = inputs.botDifficulty ?? "medium";
870
+ const globalPreset = DIFFICULTY_PRESETS[difficulty];
871
+ const gamePreset = gameAiDifficulty?.[difficulty] ?? {};
872
+ return {
873
+ strategy: inputs.botStrategy ?? gamePreset.strategy ?? globalPreset.strategy,
874
+ mctsIterations: inputs.mctsIterations ?? gamePreset.mctsIterations ?? globalPreset.mctsIterations,
875
+ delay: inputs.botDelay ?? gamePreset.delay ?? globalPreset.delay,
876
+ minDelay: inputs.botMinDelay ?? gamePreset.minDelay ?? globalPreset.minDelay,
877
+ mistakeRate: inputs.botMistakeRate ?? gamePreset.mistakeRate ?? globalPreset.mistakeRate,
878
+ customBot: inputs.customBot
879
+ };
880
+ }
881
+ function computeBotPlayerIDs(numPlayers, botCount, humanPlayerID) {
882
+ const botIDs = [];
883
+ for (let i = 0; i < numPlayers && botIDs.length < botCount; i++) {
884
+ const id = String(i);
885
+ if (id !== humanPlayerID) {
886
+ botIDs.push(id);
887
+ }
888
+ }
889
+ return botIDs;
890
+ }
891
+ var BotManager = class {
892
+ constructor(game, config) {
893
+ this.game = game;
894
+ this.bot = null;
895
+ this.pendingMove = null;
896
+ this.stateListeners = /* @__PURE__ */ new Set();
897
+ this.currentState = { isThinking: false, thinkingPlayer: null };
898
+ this.moveInProgress = false;
899
+ this.config = config;
900
+ this.initializeBot();
901
+ }
902
+ initializeBot() {
903
+ const { BoardgameAI } = window;
904
+ const enumerate = this.game.ai?.enumerate;
905
+ if (!enumerate) {
906
+ console.warn(
907
+ "[BotManager] game.ai.enumerate is required for bot support"
908
+ );
909
+ return;
910
+ }
911
+ switch (this.config.strategy) {
912
+ case "mcts":
913
+ if (BoardgameAI?.MCTSBot) {
914
+ this.bot = new BoardgameAI.MCTSBot({
915
+ game: this.game,
916
+ enumerate,
917
+ iterations: this.config.mctsIterations
918
+ });
919
+ } else {
920
+ console.warn(
921
+ "[BotManager] MCTSBot not available, falling back to RandomBot"
922
+ );
923
+ if (BoardgameAI?.RandomBot) {
924
+ this.bot = new BoardgameAI.RandomBot({
925
+ game: this.game,
926
+ enumerate
927
+ });
928
+ }
929
+ }
930
+ break;
931
+ case "random":
932
+ if (BoardgameAI?.RandomBot) {
933
+ this.bot = new BoardgameAI.RandomBot({
934
+ game: this.game,
935
+ enumerate
936
+ });
937
+ }
938
+ break;
939
+ case "custom":
940
+ if (this.config.customBot) {
941
+ this.bot = this.config.customBot();
942
+ } else {
943
+ console.error(
944
+ "[BotManager] Custom strategy requires customBot factory"
945
+ );
946
+ }
947
+ break;
948
+ }
949
+ if (!this.bot) {
950
+ console.warn(
951
+ "[BotManager] Could not initialize bot. AI module may not be loaded."
952
+ );
953
+ }
954
+ }
955
+ /**
956
+ * Select a move, potentially with mistakes for easier difficulties.
957
+ */
958
+ async selectMove(state, playerID) {
959
+ const enumerate = this.game.ai?.enumerate;
960
+ if (!enumerate) {
961
+ console.log("[BotManager] No enumerate function");
962
+ return null;
963
+ }
964
+ const legalMoves = enumerate(state.G, state.ctx);
965
+ console.log("[BotManager] Legal moves:", legalMoves);
966
+ if (legalMoves.length === 0) {
967
+ console.log("[BotManager] No legal moves available");
968
+ return null;
969
+ }
970
+ if (this.config.mistakeRate > 0 && Math.random() < this.config.mistakeRate) {
971
+ const randomIndex = Math.floor(Math.random() * legalMoves.length);
972
+ const randomMove = legalMoves[randomIndex];
973
+ return {
974
+ action: {
975
+ payload: {
976
+ type: randomMove.move,
977
+ args: randomMove.args
978
+ }
979
+ }
980
+ };
981
+ }
982
+ if (!this.bot) {
983
+ const randomIndex = Math.floor(Math.random() * legalMoves.length);
984
+ const randomMove = legalMoves[randomIndex];
985
+ return {
986
+ action: {
987
+ payload: {
988
+ type: randomMove.move,
989
+ args: randomMove.args
990
+ }
991
+ }
992
+ };
993
+ }
994
+ const result = await this.bot.play(state, playerID);
995
+ if (!result?.action && legalMoves.length > 0) {
996
+ console.log("[BotManager] Bot returned null action, falling back to random selection");
997
+ const randomIndex = Math.floor(Math.random() * legalMoves.length);
998
+ const randomMove = legalMoves[randomIndex];
999
+ return {
1000
+ action: {
1001
+ payload: {
1002
+ type: randomMove.move,
1003
+ args: randomMove.args
1004
+ }
1005
+ }
1006
+ };
1007
+ }
1008
+ return result;
1009
+ }
1010
+ /**
1011
+ * Check if it's a bot's turn and schedule a move if so.
1012
+ * Call this after every state update.
1013
+ */
1014
+ async maybePlayBot(state, botPlayerIDs, makeMove) {
1015
+ const { ctx } = state;
1016
+ console.log("[BotManager] maybePlayBot called:", {
1017
+ currentPlayer: ctx.currentPlayer,
1018
+ botPlayerIDs,
1019
+ moveInProgress: this.moveInProgress,
1020
+ gameover: ctx.gameover
1021
+ });
1022
+ if (this.moveInProgress) {
1023
+ console.log("[BotManager] Skipping - move already in progress");
1024
+ return;
1025
+ }
1026
+ const isBotTurn = botPlayerIDs.includes(ctx.currentPlayer);
1027
+ if (!isBotTurn || ctx.gameover) {
1028
+ console.log("[BotManager] Not bot turn or gameover:", { isBotTurn, gameover: ctx.gameover });
1029
+ this.cancelPendingMove();
1030
+ this.notifyState({ isThinking: false, thinkingPlayer: null });
1031
+ return;
1032
+ }
1033
+ this.moveInProgress = true;
1034
+ this.notifyState({ isThinking: true, thinkingPlayer: ctx.currentPlayer });
1035
+ const delay = this.config.minDelay > 0 ? this.config.minDelay + Math.random() * (this.config.delay - this.config.minDelay) : this.config.delay;
1036
+ try {
1037
+ console.log("[BotManager] Selecting move for player:", ctx.currentPlayer);
1038
+ const action = await this.selectMove(state, ctx.currentPlayer);
1039
+ console.log("[BotManager] Selected action:", action);
1040
+ if (!action?.action) {
1041
+ console.log("[BotManager] No action returned, skipping");
1042
+ this.notifyState({ isThinking: false, thinkingPlayer: null });
1043
+ this.moveInProgress = false;
1044
+ return;
1045
+ }
1046
+ console.log("[BotManager] Scheduling move after delay:", delay);
1047
+ this.pendingMove = setTimeout(() => {
1048
+ const { type, args } = action.action.payload;
1049
+ console.log("[BotManager] Executing move:", { type, args });
1050
+ makeMove(type, ...args);
1051
+ this.notifyState({ isThinking: false, thinkingPlayer: null });
1052
+ this.moveInProgress = false;
1053
+ }, delay);
1054
+ } catch (error) {
1055
+ console.error("[BotManager] Error computing move:", error);
1056
+ this.notifyState({ isThinking: false, thinkingPlayer: null });
1057
+ this.moveInProgress = false;
1058
+ }
1059
+ }
1060
+ /** Subscribe to bot state changes */
1061
+ subscribe(listener) {
1062
+ this.stateListeners.add(listener);
1063
+ listener(this.currentState);
1064
+ return () => this.stateListeners.delete(listener);
1065
+ }
1066
+ /** Get current bot state */
1067
+ getState() {
1068
+ return this.currentState;
1069
+ }
1070
+ notifyState(state) {
1071
+ this.currentState = state;
1072
+ this.stateListeners.forEach((fn) => fn(state));
1073
+ }
1074
+ cancelPendingMove() {
1075
+ if (this.pendingMove) {
1076
+ clearTimeout(this.pendingMove);
1077
+ this.pendingMove = null;
1078
+ }
1079
+ this.moveInProgress = false;
1080
+ }
1081
+ dispose() {
1082
+ this.cancelPendingMove();
1083
+ this.stateListeners.clear();
1084
+ }
1085
+ };
1086
+
1087
+ // src/mount.ts
1088
+ function createGameMount(game, Board, options = {}) {
1089
+ const {
1090
+ numPlayers: defaultNumPlayers = game.maxPlayers ?? game.minPlayers ?? 2,
1091
+ defaultPlayerID = "0"
1092
+ } = options;
1093
+ return (container, inputs = {}) => {
1094
+ const { BoardgameReact, BoardgameMultiplayer, React: R, ReactDOM } = window;
1095
+ if (!BoardgameReact || !R || !ReactDOM) {
1096
+ console.error(
1097
+ "[boardgameio] Missing globals: BoardgameReact, React, or ReactDOM"
1098
+ );
1099
+ container.innerHTML = '<div style="color: red; padding: 16px;">Missing required dependencies</div>';
1100
+ return () => {
1101
+ };
1102
+ }
1103
+ const numPlayers = inputs.numPlayers ?? defaultNumPlayers;
1104
+ const multiplayerConfig = inputs.multiplayer;
1105
+ const isMultiplayer = !!multiplayerConfig?.matchID;
1106
+ const botCountInput = inputs["bot-count"] ?? inputs.botCount;
1107
+ const botCount = typeof botCountInput === "number" ? botCountInput : isMultiplayer ? 0 : numPlayers - 1;
1108
+ const playerID = inputs.playerID ?? multiplayerConfig?.playerID ?? defaultPlayerID;
1109
+ const botPlayerIDs = computeBotPlayerIDs(numPlayers, botCount, playerID);
1110
+ console.log("[boardgameio] Bot configuration:", {
1111
+ numPlayers,
1112
+ botCount,
1113
+ botCountInput,
1114
+ isMultiplayer,
1115
+ playerID,
1116
+ botPlayerIDs,
1117
+ hasBotEnumerate: !!game.ai?.enumerate,
1118
+ inputsBotCount: inputs["bot-count"],
1119
+ hasMultiplayerConfig: !!multiplayerConfig
1120
+ });
1121
+ const botDifficulty = inputs.botDifficulty ?? "medium";
1122
+ const botConfig = resolveBotConfig(inputs, game.ai?.difficulty);
1123
+ const hasBots = botCount > 0 && game.ai?.enumerate;
1124
+ let botManager = null;
1125
+ if (hasBots) {
1126
+ botManager = new BotManager(game, botConfig);
1127
+ }
1128
+ let multiplayer;
1129
+ if (isMultiplayer && multiplayerConfig) {
1130
+ const isHost = multiplayerConfig.isHost ?? playerID === "0";
1131
+ multiplayer = createP2PTransport({
1132
+ isHost
1133
+ });
1134
+ }
1135
+ const WrappedBoard = (props) => {
1136
+ const [botState, setBotState] = R.useState({
1137
+ isThinking: false,
1138
+ thinkingPlayer: null
1139
+ });
1140
+ R.useEffect(() => {
1141
+ if (!botManager) return;
1142
+ return botManager.subscribe(setBotState);
1143
+ }, []);
1144
+ R.useEffect(() => {
1145
+ if (isMultiplayer) {
1146
+ return;
1147
+ }
1148
+ if (!botManager || !props.G) {
1149
+ console.log("[boardgameio] Bot effect skipped:", {
1150
+ hasBotManager: !!botManager,
1151
+ hasG: !!props.G
1152
+ });
1153
+ return;
1154
+ }
1155
+ const gState = props.G;
1156
+ const ctxState = props.ctx;
1157
+ const currentPlayer = gState.current !== void 0 ? String(gState.current) : ctxState.currentPlayer;
1158
+ const gameover = gState.winner !== void 0 ? gState.winner !== null : ctxState.gameover;
1159
+ const isBotTurn = botPlayerIDs.includes(currentPlayer);
1160
+ console.log("[boardgameio] Bot effect running:", {
1161
+ currentPlayer,
1162
+ botPlayerIDs,
1163
+ isBotTurn,
1164
+ gameover
1165
+ });
1166
+ const state = {
1167
+ G: props.G,
1168
+ ctx: {
1169
+ ...ctxState,
1170
+ currentPlayer,
1171
+ gameover
1172
+ }
1173
+ };
1174
+ botManager.maybePlayBot(state, botPlayerIDs, (type, ...args) => {
1175
+ console.log("[boardgameio] Bot making move:", { type, args });
1176
+ const moves = props.moves;
1177
+ moves[type]?.(...args);
1178
+ });
1179
+ }, [
1180
+ props.G,
1181
+ props.ctx?.currentPlayer,
1182
+ props.ctx?.turn,
1183
+ props.G?.current
1184
+ ]);
1185
+ return R.createElement(Board, {
1186
+ ...props,
1187
+ isMultiplayer,
1188
+ botState,
1189
+ botPlayerIDs,
1190
+ botCount,
1191
+ botDifficulty
1192
+ });
1193
+ };
1194
+ const GameClient = BoardgameReact.Client({
1195
+ game,
1196
+ board: WrappedBoard,
1197
+ numPlayers,
1198
+ ...multiplayer ? { multiplayer } : {}
1199
+ });
1200
+ const GameWithSettings = () => {
1201
+ const clientProps = { playerID };
1202
+ if (isMultiplayer && multiplayerConfig) {
1203
+ clientProps.matchID = multiplayerConfig.matchID;
1204
+ clientProps.credentials = multiplayerConfig.credentials;
1205
+ console.log("[boardgameio] Multiplayer props:", {
1206
+ matchID: multiplayerConfig.matchID,
1207
+ playerID,
1208
+ isHost: multiplayerConfig.isHost ?? playerID === "0"
1209
+ });
1210
+ }
1211
+ return R.createElement(
1212
+ SettingsProvider,
1213
+ { settings: inputs },
1214
+ R.createElement(GameClient, clientProps)
1215
+ );
1216
+ };
1217
+ const root = ReactDOM.createRoot(container);
1218
+ root.render(R.createElement(GameWithSettings));
1219
+ return () => {
1220
+ botManager?.dispose();
1221
+ root.unmount();
1222
+ };
1223
+ };
1224
+ }
1225
+ function createMountFromExports(module, manifest) {
1226
+ const { game, app, default: defaultExport } = module;
1227
+ if (!game || typeof game.setup !== "function") {
1228
+ return null;
1229
+ }
1230
+ const Board = app || defaultExport;
1231
+ if (!Board || typeof Board !== "function") {
1232
+ return null;
1233
+ }
1234
+ const numPlayers = manifest?.players?.max ?? manifest?.players?.min ?? game.maxPlayers ?? game.minPlayers ?? 2;
1235
+ return createGameMount(game, Board, { numPlayers });
1236
+ }
1237
+ function injectMountHelper() {
1238
+ const win = window;
1239
+ win.createGameMount = createGameMount;
1240
+ win.createMountFromExports = createMountFromExports;
1241
+ win.useSettings = useSettings;
1242
+ win.SettingsProvider = SettingsProvider;
1243
+ }
1244
+ async function mount(module, container, inputs) {
1245
+ const { React: R, ReactDOM } = window;
1246
+ if (typeof module.mount === "function") {
1247
+ const mountFn = module.mount;
1248
+ const result = await mountFn(container, inputs);
1249
+ if (typeof result === "function") {
1250
+ return result;
1251
+ }
1252
+ return;
1253
+ }
1254
+ const game = module.game;
1255
+ if (game && typeof game.setup === "function") {
1256
+ const Board = module.app || module.default;
1257
+ if (Board && typeof Board === "function") {
1258
+ const numPlayers = inputs.numPlayers ?? game.maxPlayers ?? game.minPlayers ?? 2;
1259
+ const multiplayerInput = inputs.multiplayer;
1260
+ if (multiplayerInput?.matchID) {
1261
+ await ensurePeerJS();
1262
+ }
1263
+ const gameMount = createGameMount(game, Board, { numPlayers });
1264
+ return gameMount(container, inputs);
1265
+ }
1266
+ }
1267
+ if (typeof module.default === "function" && R && ReactDOM) {
1268
+ const Component = module.default;
1269
+ const root = ReactDOM.createRoot(container);
1270
+ root.render(R.createElement(Component, inputs));
1271
+ return () => root.unmount();
1272
+ }
1273
+ console.warn(
1274
+ "[boardgameio] Widget does not export a recognized entry point (mount, game+app, or default)"
1275
+ );
1276
+ }
1277
+
1278
+ // src/setup.ts
1279
+ var tailwindLoadPromise = null;
1280
+ var peerJSLoadPromise = null;
1281
+ var mountHelperInjected = false;
1282
+ async function setup(container, options = {}) {
1283
+ const { cssRuntime = true, multiplayer = false } = options;
1284
+ if (!mountHelperInjected) {
1285
+ injectMountHelper();
1286
+ mountHelperInjected = true;
1287
+ }
1288
+ if (cssRuntime && !tailwindLoadPromise) {
1289
+ tailwindLoadPromise = loadTailwindPlayCDN();
1290
+ }
1291
+ if (multiplayer && !peerJSLoadPromise) {
1292
+ peerJSLoadPromise = loadPeerJS();
1293
+ }
1294
+ await Promise.all([tailwindLoadPromise, peerJSLoadPromise].filter(Boolean));
1295
+ }
1296
+ function cleanup(container) {
1297
+ }
1298
+ async function loadTailwindPlayCDN() {
1299
+ if (document.querySelector('script[src*="tailwindcss.com/play"]')) {
1300
+ return;
1301
+ }
1302
+ const script = document.createElement("script");
1303
+ script.src = "https://cdn.tailwindcss.com";
1304
+ script.async = true;
1305
+ return new Promise((resolve, reject) => {
1306
+ script.onload = () => resolve();
1307
+ script.onerror = () => reject(new Error("Failed to load Tailwind CDN"));
1308
+ document.head.appendChild(script);
1309
+ });
1310
+ }
1311
+ async function loadPeerJS() {
1312
+ if (window.Peer) {
1313
+ return;
1314
+ }
1315
+ if (document.querySelector('script[src*="peerjs"]')) {
1316
+ return new Promise((resolve) => {
1317
+ const check = () => {
1318
+ if (window.Peer) resolve();
1319
+ else setTimeout(check, 50);
1320
+ };
1321
+ check();
1322
+ });
1323
+ }
1324
+ const script = document.createElement("script");
1325
+ script.src = "https://unpkg.com/peerjs@1.5.4/dist/peerjs.min.js";
1326
+ script.async = true;
1327
+ return new Promise((resolve, reject) => {
1328
+ script.onload = () => resolve();
1329
+ script.onerror = () => reject(new Error("Failed to load PeerJS"));
1330
+ document.head.appendChild(script);
1331
+ });
1332
+ }
1333
+ async function ensurePeerJS() {
1334
+ if (!peerJSLoadPromise) {
1335
+ peerJSLoadPromise = loadPeerJS();
1336
+ }
1337
+ await peerJSLoadPromise;
1338
+ }
1339
+
1340
+ // src/multiplayer.ts
1341
+ function getMultiplayer(config, game) {
1342
+ const BoardgameMultiplayer = window.BoardgameMultiplayer;
1343
+ const BoardgameAI = window.BoardgameAI;
1344
+ if (config.isMultiplayer) {
1345
+ return void 0;
1346
+ }
1347
+ const bots = {};
1348
+ const botCount = config.botCount ?? 0;
1349
+ if (botCount > 0 && game?.ai && BoardgameAI?.MCTSBot) {
1350
+ for (let i = 1; i <= botCount; i++) {
1351
+ bots[String(i)] = BoardgameAI.MCTSBot;
1352
+ }
1353
+ }
1354
+ if (!BoardgameMultiplayer?.Local) {
1355
+ return void 0;
1356
+ }
1357
+ return BoardgameMultiplayer.Local({
1358
+ storageKey: `zolvery:bgio:${config.appId}`,
1359
+ ...Object.keys(bots).length > 0 ? { bots } : {}
1360
+ });
1361
+ }
1362
+ function generateMatchID() {
1363
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
1364
+ return Array.from(
1365
+ { length: 6 },
1366
+ () => chars[Math.floor(Math.random() * chars.length)]
1367
+ ).join("");
1368
+ }
1369
+ export {
1370
+ BotManager,
1371
+ DIFFICULTY_PRESETS,
1372
+ P2PDB,
1373
+ P2PHost,
1374
+ P2PTransport,
1375
+ SettingsProvider,
1376
+ cleanup,
1377
+ computeBotPlayerIDs,
1378
+ createGameMount,
1379
+ createMountFromExports,
1380
+ createP2PTransport,
1381
+ ensurePeerJS,
1382
+ generateCredentials,
1383
+ generateKeyPair,
1384
+ generateMatchID,
1385
+ getMultiplayer,
1386
+ injectMountHelper,
1387
+ mount,
1388
+ resolveBotConfig,
1389
+ setup,
1390
+ useSettings
1391
+ };
1392
+ //# sourceMappingURL=index.js.map