@kossabos/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,903 @@
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/host.ts
144
+ var P2PHost = class {
145
+ constructor({
146
+ game,
147
+ numPlayers = 2,
148
+ matchID
149
+ }) {
150
+ this.clients = /* @__PURE__ */ new Map();
151
+ this.hostClient = null;
152
+ this.matchID = matchID;
153
+ this.game = game;
154
+ this.numPlayers = numPlayers;
155
+ this.db = new P2PDB();
156
+ const initialState = this.createInitialState();
157
+ this.state = initialState;
158
+ const players = {};
159
+ for (let i = 0; i < numPlayers; i++) {
160
+ players[i] = { id: i };
161
+ }
162
+ this.db.createMatch(matchID, {
163
+ initialState,
164
+ metadata: {
165
+ gameName: game.name ?? "unknown",
166
+ players,
167
+ createdAt: Date.now(),
168
+ updatedAt: Date.now()
169
+ }
170
+ });
171
+ }
172
+ createInitialState() {
173
+ const activePlayers = {};
174
+ for (let i = 0; i < this.numPlayers; i++) {
175
+ activePlayers[String(i)] = "";
176
+ }
177
+ const ctx = {
178
+ numPlayers: this.numPlayers,
179
+ turn: 1,
180
+ currentPlayer: "0",
181
+ playOrder: Array.from({ length: this.numPlayers }, (_, i) => String(i)),
182
+ playOrderPos: 0,
183
+ phase: "",
184
+ activePlayers
185
+ };
186
+ const G = this.game.setup?.({ ctx, ...ctx }) ?? {};
187
+ return {
188
+ G,
189
+ ctx,
190
+ plugins: {},
191
+ _stateID: 0,
192
+ _undo: [],
193
+ _redo: []
194
+ };
195
+ }
196
+ registerClient(client) {
197
+ if (!authenticate(this.matchID, client.metadata, this.db)) {
198
+ console.log(
199
+ "[P2PHost] Client auth failed for playerID:",
200
+ client.metadata.playerID
201
+ );
202
+ return;
203
+ }
204
+ console.log(
205
+ "[P2PHost] Registered client for playerID:",
206
+ client.metadata.playerID
207
+ );
208
+ this.clients.set(client, client);
209
+ this.syncClient(client);
210
+ }
211
+ registerHostClient(client) {
212
+ console.log(
213
+ "[P2PHost] Registered host client for playerID:",
214
+ client.metadata.playerID
215
+ );
216
+ this.hostClient = client;
217
+ this.syncClient(client);
218
+ }
219
+ unregisterClient(client) {
220
+ this.clients.delete(client);
221
+ }
222
+ processAction(client, data) {
223
+ switch (data.type) {
224
+ case "sync":
225
+ this.syncClient(client);
226
+ break;
227
+ case "update":
228
+ this.handleUpdate(data.args);
229
+ break;
230
+ case "chat":
231
+ this.broadcastChat(data.args);
232
+ break;
233
+ }
234
+ }
235
+ syncClient(client) {
236
+ const { state, log } = this.db.fetch(this.matchID);
237
+ const playerID = client.metadata.playerID;
238
+ const filteredState = this.filterStateForPlayer(state, playerID);
239
+ client.send({
240
+ type: "sync",
241
+ args: [this.matchID, { state: filteredState, log }]
242
+ });
243
+ }
244
+ filterStateForPlayer(state, _playerID) {
245
+ return state;
246
+ }
247
+ handleUpdate([matchID, , action]) {
248
+ if (matchID !== this.matchID) return;
249
+ const currentState = this.state;
250
+ if (!currentState) return;
251
+ const moveName = action.payload?.type;
252
+ const moveArgs = action.payload?.args ?? [];
253
+ const playerID = action.payload?.playerID;
254
+ console.log("[P2PHost] handleUpdate:", { moveName, moveArgs, playerID });
255
+ if (moveName && this.game.moves?.[moveName]) {
256
+ const move = this.game.moves[moveName];
257
+ const ctx = { ...currentState.ctx, playerID };
258
+ const G = JSON.parse(JSON.stringify(currentState.G));
259
+ console.log(
260
+ "[P2PHost] Before move, G.players:",
261
+ G.players?.map((p) => ({
262
+ id: p.id,
263
+ bet: p.bet
264
+ }))
265
+ );
266
+ if (typeof move === "function") {
267
+ move({ G, ctx }, ...moveArgs);
268
+ }
269
+ console.log(
270
+ "[P2PHost] After move, G.players:",
271
+ G.players?.map((p) => ({
272
+ id: p.id,
273
+ bet: p.bet
274
+ }))
275
+ );
276
+ const newState = {
277
+ ...currentState,
278
+ G,
279
+ _stateID: currentState._stateID + 1
280
+ };
281
+ this.state = newState;
282
+ this.db.setState(this.matchID, newState);
283
+ console.log(
284
+ "[P2PHost] State updated, broadcasting to",
285
+ this.clients.size,
286
+ "clients"
287
+ );
288
+ this.broadcastState();
289
+ } else {
290
+ console.log(
291
+ "[P2PHost] Move not found:",
292
+ moveName,
293
+ "Available:",
294
+ Object.keys(this.game.moves || {})
295
+ );
296
+ }
297
+ }
298
+ broadcastState() {
299
+ const { state, log } = this.db.fetch(this.matchID);
300
+ if (this.hostClient) {
301
+ const playerID = this.hostClient.metadata.playerID;
302
+ const filteredState = this.filterStateForPlayer(state, playerID);
303
+ this.hostClient.send({
304
+ type: "sync",
305
+ args: [this.matchID, { state: filteredState, log }]
306
+ });
307
+ }
308
+ for (const client of this.clients.values()) {
309
+ const playerID = client.metadata.playerID;
310
+ const filteredState = this.filterStateForPlayer(state, playerID);
311
+ client.send({
312
+ type: "sync",
313
+ args: [this.matchID, { state: filteredState, log }]
314
+ });
315
+ }
316
+ }
317
+ broadcastChat(args) {
318
+ if (this.hostClient) {
319
+ this.hostClient.send({ type: "chat", args });
320
+ }
321
+ for (const client of this.clients.values()) {
322
+ client.send({ type: "chat", args });
323
+ }
324
+ }
325
+ };
326
+
327
+ // src/p2p/transport.ts
328
+ var DEFAULT_ICE_SERVERS = [
329
+ { urls: "stun:stun.l.google.com:19302" },
330
+ { urls: "stun:stun1.l.google.com:19302" },
331
+ { urls: "stun:stun2.l.google.com:19302" },
332
+ // Free TURN servers from Open Relay Project
333
+ {
334
+ urls: "turn:openrelay.metered.ca:80",
335
+ username: "openrelayproject",
336
+ credential: "openrelayproject"
337
+ },
338
+ {
339
+ urls: "turn:openrelay.metered.ca:443",
340
+ username: "openrelayproject",
341
+ credential: "openrelayproject"
342
+ },
343
+ {
344
+ urls: "turn:openrelay.metered.ca:443?transport=tcp",
345
+ username: "openrelayproject",
346
+ credential: "openrelayproject"
347
+ }
348
+ ];
349
+ var P2PTransport = class {
350
+ constructor(config, opts = {}) {
351
+ this.peer = null;
352
+ this.connection = null;
353
+ this.host = null;
354
+ this.connected = false;
355
+ this.connectionStatusCallbacks = /* @__PURE__ */ new Set();
356
+ this.retryCount = 0;
357
+ this.maxRetries = 3;
358
+ console.log("[P2PTransport] Constructor called with config:", {
359
+ gameName: config.gameName,
360
+ playerID: config.playerID,
361
+ matchID: config.matchID,
362
+ numPlayers: config.numPlayers,
363
+ credentials: config.credentials ? "(present)" : "(none)"
364
+ });
365
+ console.log("[P2PTransport] Options:", {
366
+ isHost: opts.isHost
367
+ });
368
+ this.gameName = config.gameName;
369
+ this.playerID = config.playerID;
370
+ this.matchID = config.matchID;
371
+ this.numPlayers = config.numPlayers;
372
+ this.game = config.game;
373
+ this.transportDataCallback = config.transportDataCallback;
374
+ this.isHost = opts.isHost ?? false;
375
+ this.peerOptions = opts.peerOptions;
376
+ this.onError = opts.onError;
377
+ this.setCredentials(config.credentials);
378
+ }
379
+ get hostID() {
380
+ return `boardgameio-${this.gameName}-matchid-${this.matchID}`;
381
+ }
382
+ setCredentials(credentials) {
383
+ if (!credentials) {
384
+ const { publicKey, privateKey } = generateKeyPair();
385
+ this.credentials = publicKey;
386
+ this.privateKey = privateKey;
387
+ } else {
388
+ this.credentials = credentials;
389
+ }
390
+ }
391
+ get metadata() {
392
+ return {
393
+ playerID: this.playerID,
394
+ credentials: this.credentials,
395
+ message: this.privateKey && this.playerID ? signMessage(this.playerID, this.privateKey) : void 0
396
+ };
397
+ }
398
+ connect() {
399
+ const Peer = window.Peer;
400
+ if (!Peer) {
401
+ this.onError?.(new Error("PeerJS not loaded"));
402
+ return;
403
+ }
404
+ const globalPeerConfig = window.__peerConfig ?? {};
405
+ const iceServers = globalPeerConfig.iceServers && globalPeerConfig.iceServers.length > 0 ? globalPeerConfig.iceServers : DEFAULT_ICE_SERVERS;
406
+ const baseConfig = {
407
+ host: globalPeerConfig.host,
408
+ port: globalPeerConfig.port,
409
+ path: globalPeerConfig.path,
410
+ secure: globalPeerConfig.secure,
411
+ debug: 2,
412
+ // Warnings and errors
413
+ config: {
414
+ iceServers
415
+ }
416
+ };
417
+ const peerConfig = {
418
+ ...baseConfig,
419
+ ...this.peerOptions,
420
+ config: {
421
+ ...baseConfig.config,
422
+ ...this.peerOptions && this.peerOptions.config ? this.peerOptions.config : {}
423
+ }
424
+ };
425
+ console.log(
426
+ `[P2PTransport] Connecting as ${this.isHost ? "HOST" : "CLIENT"}, hostID: ${this.hostID}`
427
+ );
428
+ this.peer = new Peer(this.isHost ? this.hostID : void 0, peerConfig);
429
+ this.peer.on("open", (id) => {
430
+ console.log(`[P2PTransport] Peer opened with ID: ${id}`);
431
+ if (this.isHost) {
432
+ this.host = new P2PHost({
433
+ game: this.game,
434
+ numPlayers: this.numPlayers,
435
+ matchID: this.matchID
436
+ });
437
+ this.host.registerHostClient({
438
+ metadata: this.metadata,
439
+ send: (data) => this.notifyClient(data)
440
+ });
441
+ console.log("[P2PTransport] Host ready, waiting for connections");
442
+ this.onConnect();
443
+ } else {
444
+ console.log(`[P2PTransport] Client connecting to host: ${this.hostID}`);
445
+ this.connectToHost();
446
+ }
447
+ });
448
+ this.peer.on("connection", (conn) => {
449
+ console.log(
450
+ "[P2PTransport] Incoming connection from:",
451
+ conn.peer
452
+ );
453
+ const dataConn = conn;
454
+ if (!this.host) return;
455
+ const client = {
456
+ metadata: { playerID: null },
457
+ send: (data) => dataConn.send(data)
458
+ };
459
+ dataConn.on("open", () => {
460
+ console.log("[P2PTransport] Data connection opened");
461
+ this.host?.registerClient(client);
462
+ });
463
+ dataConn.on("data", (data) => {
464
+ const action = data;
465
+ if (action.type === "sync" && "metadata" in data) {
466
+ client.metadata = data.metadata;
467
+ }
468
+ this.host?.processAction(client, action);
469
+ });
470
+ dataConn.on("close", () => {
471
+ this.host?.unregisterClient(client);
472
+ });
473
+ });
474
+ this.peer.on("error", (err) => {
475
+ const error = err;
476
+ console.error("[P2PTransport] Peer error:", error.type, error.message);
477
+ if (error.type === "peer-unavailable") {
478
+ console.error(
479
+ "[P2PTransport] Host peer not found. Is the host connected?"
480
+ );
481
+ } else if (error.type === "unavailable-id") {
482
+ console.error("[P2PTransport] Peer ID already taken");
483
+ }
484
+ this.onError?.(error);
485
+ });
486
+ this.peer.on("close", () => {
487
+ console.log("[P2PTransport] Peer connection closed");
488
+ this.connected = false;
489
+ this.notifyConnectionStatus(false);
490
+ });
491
+ }
492
+ connectToHost() {
493
+ if (!this.peer) return;
494
+ console.log(
495
+ `[P2PTransport] Attempting connection to host (attempt ${this.retryCount + 1}/${this.maxRetries + 1})`
496
+ );
497
+ this.connection = this.peer.connect(this.hostID, {
498
+ reliable: true,
499
+ serialization: "json"
500
+ });
501
+ const connectionTimeout = setTimeout(() => {
502
+ if (!this.connected && this.connection) {
503
+ this.retryCount++;
504
+ if (this.retryCount <= this.maxRetries) {
505
+ console.warn(
506
+ `[P2PTransport] Connection timeout, retrying (${this.retryCount}/${this.maxRetries})...`
507
+ );
508
+ this.connection.close();
509
+ this.connectToHost();
510
+ } else {
511
+ console.error(
512
+ "[P2PTransport] Max retries reached. Could not connect to host."
513
+ );
514
+ this.onError?.(
515
+ new Error(
516
+ "Could not connect to host after multiple attempts. Is the host online?"
517
+ )
518
+ );
519
+ }
520
+ }
521
+ }, 1e4);
522
+ this.connection.on("open", () => {
523
+ clearTimeout(connectionTimeout);
524
+ console.log("[P2PTransport] Connected to host successfully!");
525
+ this.retryCount = 0;
526
+ this.connection?.send({ type: "sync", metadata: this.metadata });
527
+ this.onConnect();
528
+ });
529
+ this.connection.on("data", (data) => {
530
+ this.notifyClient(data);
531
+ });
532
+ this.connection.on("close", () => {
533
+ clearTimeout(connectionTimeout);
534
+ console.log("[P2PTransport] Connection to host closed");
535
+ this.connected = false;
536
+ this.notifyConnectionStatus(false);
537
+ });
538
+ this.connection.on("error", (err) => {
539
+ clearTimeout(connectionTimeout);
540
+ const error = err;
541
+ console.error(
542
+ "[P2PTransport] Connection error:",
543
+ error.type,
544
+ error.message
545
+ );
546
+ this.onError?.(error);
547
+ });
548
+ }
549
+ onConnect() {
550
+ this.connected = true;
551
+ this.notifyConnectionStatus(true);
552
+ this.requestSync();
553
+ }
554
+ notifyConnectionStatus(connected) {
555
+ for (const callback of this.connectionStatusCallbacks) {
556
+ callback(connected);
557
+ }
558
+ }
559
+ notifyClient(data) {
560
+ this.transportDataCallback(data);
561
+ }
562
+ disconnect() {
563
+ this.connection?.close();
564
+ this.peer?.destroy();
565
+ this.peer = null;
566
+ this.connection = null;
567
+ this.host = null;
568
+ this.connected = false;
569
+ this.notifyConnectionStatus(false);
570
+ }
571
+ requestSync() {
572
+ if (this.isHost && this.host) {
573
+ this.host.processAction(
574
+ {
575
+ metadata: this.metadata,
576
+ send: (d) => this.notifyClient(d)
577
+ },
578
+ { type: "sync" }
579
+ );
580
+ } else {
581
+ this.connection?.send({ type: "sync", metadata: this.metadata });
582
+ }
583
+ }
584
+ sendAction(state, action) {
585
+ const msg = {
586
+ type: "update",
587
+ args: [this.matchID, state, action]
588
+ };
589
+ if (this.isHost && this.host) {
590
+ this.host.processAction(
591
+ {
592
+ metadata: this.metadata,
593
+ send: (d) => this.notifyClient(d)
594
+ },
595
+ msg
596
+ );
597
+ } else {
598
+ this.connection?.send(msg);
599
+ }
600
+ }
601
+ sendChatMessage(matchID, chatMessage) {
602
+ const msg = {
603
+ type: "chat",
604
+ args: [matchID, chatMessage, this.credentials]
605
+ };
606
+ if (this.isHost && this.host) {
607
+ this.host.processAction(
608
+ {
609
+ metadata: this.metadata,
610
+ send: (d) => this.notifyClient(d)
611
+ },
612
+ msg
613
+ );
614
+ } else {
615
+ this.connection?.send(msg);
616
+ }
617
+ }
618
+ updateMatchID(id) {
619
+ this.matchID = id;
620
+ this.disconnect();
621
+ this.connect();
622
+ }
623
+ updatePlayerID(id) {
624
+ this.playerID = id;
625
+ this.disconnect();
626
+ this.connect();
627
+ }
628
+ updateCredentials(credentials) {
629
+ this.setCredentials(credentials);
630
+ this.disconnect();
631
+ this.connect();
632
+ }
633
+ isConnected() {
634
+ return this.connected;
635
+ }
636
+ subscribeToConnectionStatus(callback) {
637
+ this.connectionStatusCallbacks.add(callback);
638
+ callback(this.connected);
639
+ return () => {
640
+ this.connectionStatusCallbacks.delete(callback);
641
+ };
642
+ }
643
+ };
644
+ function createP2PTransport(opts = {}) {
645
+ return (config) => new P2PTransport(config, opts);
646
+ }
647
+
648
+ // src/mount.ts
649
+ function createGameMount(game, Board, options = {}) {
650
+ const {
651
+ numPlayers = game.maxPlayers ?? game.minPlayers ?? 2,
652
+ defaultPlayerID = "0"
653
+ } = options;
654
+ return (container, inputs = {}) => {
655
+ const {
656
+ BoardgameReact,
657
+ BoardgameAI,
658
+ BoardgameMultiplayer,
659
+ React: R,
660
+ ReactDOM
661
+ } = window;
662
+ if (!BoardgameReact || !R || !ReactDOM) {
663
+ console.error(
664
+ "[boardgameio] Missing globals: BoardgameReact, React, or ReactDOM"
665
+ );
666
+ container.innerHTML = '<div style="color: red; padding: 16px;">Missing required dependencies</div>';
667
+ return () => {
668
+ };
669
+ }
670
+ const botCount = typeof inputs["bot-count"] === "number" ? inputs["bot-count"] : 0;
671
+ const canUseBots = botCount > 0 && !!game.ai;
672
+ const botFactory = canUseBots ? BoardgameAI?.RandomBot : void 0;
673
+ if (canUseBots && !botFactory) {
674
+ console.warn(
675
+ "[boardgameio] Bot settings enabled, but AI module is unavailable."
676
+ );
677
+ }
678
+ const enumerate = typeof game.ai?.enumerate === "function" ? game.ai.enumerate : void 0;
679
+ if (canUseBots && !enumerate) {
680
+ console.warn(
681
+ "[boardgameio] Bot settings enabled, but game.ai.enumerate is missing."
682
+ );
683
+ }
684
+ const botPlayers = {};
685
+ if (botFactory && enumerate) {
686
+ let added = 0;
687
+ for (let pid = 0; pid < numPlayers && added < botCount; pid += 1) {
688
+ const pidStr = String(pid);
689
+ if (pidStr === defaultPlayerID) continue;
690
+ botPlayers[pidStr] = botFactory;
691
+ added += 1;
692
+ }
693
+ }
694
+ const multiplayerConfig = inputs.multiplayer;
695
+ const isMultiplayer = !!multiplayerConfig?.matchID;
696
+ const playerID = multiplayerConfig?.playerID ?? defaultPlayerID;
697
+ let multiplayer;
698
+ if (isMultiplayer && multiplayerConfig) {
699
+ const isHost = multiplayerConfig.isHost ?? playerID === "0";
700
+ multiplayer = createP2PTransport({
701
+ isHost
702
+ });
703
+ } else if (BoardgameMultiplayer?.Local) {
704
+ multiplayer = BoardgameMultiplayer.Local(
705
+ Object.keys(botPlayers).length > 0 ? { bots: botPlayers } : void 0
706
+ );
707
+ }
708
+ const WrappedBoard = (props) => {
709
+ return R.createElement(Board, { ...props, isMultiplayer });
710
+ };
711
+ const GameClient = BoardgameReact.Client({
712
+ game,
713
+ board: WrappedBoard,
714
+ numPlayers,
715
+ ...multiplayer ? { multiplayer } : {}
716
+ // Bots are configured via Local transport when enabled.
717
+ });
718
+ const GameWithSettings = () => {
719
+ const clientProps = { playerID };
720
+ if (isMultiplayer && multiplayerConfig) {
721
+ clientProps.matchID = multiplayerConfig.matchID;
722
+ clientProps.credentials = multiplayerConfig.credentials;
723
+ console.log("[boardgameio] Multiplayer props:", {
724
+ matchID: multiplayerConfig.matchID,
725
+ playerID,
726
+ isHost: multiplayerConfig.isHost ?? playerID === "0"
727
+ });
728
+ }
729
+ return R.createElement(
730
+ SettingsProvider,
731
+ { settings: inputs },
732
+ R.createElement(GameClient, clientProps)
733
+ );
734
+ };
735
+ const root = ReactDOM.createRoot(container);
736
+ root.render(R.createElement(GameWithSettings));
737
+ return () => root.unmount();
738
+ };
739
+ }
740
+ function createMountFromExports(module, manifest) {
741
+ const { game, app, default: defaultExport } = module;
742
+ if (!game || typeof game.setup !== "function") {
743
+ return null;
744
+ }
745
+ const Board = app || defaultExport;
746
+ if (!Board || typeof Board !== "function") {
747
+ return null;
748
+ }
749
+ const numPlayers = manifest?.players?.max ?? manifest?.players?.min ?? game.maxPlayers ?? game.minPlayers ?? 2;
750
+ return createGameMount(game, Board, { numPlayers });
751
+ }
752
+ function injectMountHelper() {
753
+ const win = window;
754
+ win.createGameMount = createGameMount;
755
+ win.createMountFromExports = createMountFromExports;
756
+ win.useSettings = useSettings;
757
+ win.SettingsProvider = SettingsProvider;
758
+ }
759
+ async function mount(module, container, inputs) {
760
+ const { React: R, ReactDOM } = window;
761
+ if (typeof module.mount === "function") {
762
+ const mountFn = module.mount;
763
+ const result = await mountFn(container, inputs);
764
+ if (typeof result === "function") {
765
+ return result;
766
+ }
767
+ return;
768
+ }
769
+ const game = module.game;
770
+ if (game && typeof game.setup === "function") {
771
+ const Board = module.app || module.default;
772
+ if (Board && typeof Board === "function") {
773
+ const numPlayers = inputs.numPlayers ?? game.maxPlayers ?? game.minPlayers ?? 2;
774
+ const multiplayerInput = inputs.multiplayer;
775
+ if (multiplayerInput?.matchID) {
776
+ await ensurePeerJS();
777
+ }
778
+ const gameMount = createGameMount(game, Board, { numPlayers });
779
+ return gameMount(container, inputs);
780
+ }
781
+ }
782
+ if (typeof module.default === "function" && R && ReactDOM) {
783
+ const Component = module.default;
784
+ const root = ReactDOM.createRoot(container);
785
+ root.render(R.createElement(Component, inputs));
786
+ return () => root.unmount();
787
+ }
788
+ console.warn(
789
+ "[boardgameio] Widget does not export a recognized entry point (mount, game+app, or default)"
790
+ );
791
+ }
792
+
793
+ // src/setup.ts
794
+ var tailwindLoadPromise = null;
795
+ var peerJSLoadPromise = null;
796
+ var mountHelperInjected = false;
797
+ async function setup(container, options = {}) {
798
+ const { cssRuntime = true, multiplayer = false } = options;
799
+ if (!mountHelperInjected) {
800
+ injectMountHelper();
801
+ mountHelperInjected = true;
802
+ }
803
+ if (cssRuntime && !tailwindLoadPromise) {
804
+ tailwindLoadPromise = loadTailwindPlayCDN();
805
+ }
806
+ if (multiplayer && !peerJSLoadPromise) {
807
+ peerJSLoadPromise = loadPeerJS();
808
+ }
809
+ await Promise.all([tailwindLoadPromise, peerJSLoadPromise].filter(Boolean));
810
+ }
811
+ function cleanup(container) {
812
+ }
813
+ async function loadTailwindPlayCDN() {
814
+ if (document.querySelector('script[src*="tailwindcss.com/play"]')) {
815
+ return;
816
+ }
817
+ const script = document.createElement("script");
818
+ script.src = "https://cdn.tailwindcss.com";
819
+ script.async = true;
820
+ return new Promise((resolve, reject) => {
821
+ script.onload = () => resolve();
822
+ script.onerror = () => reject(new Error("Failed to load Tailwind CDN"));
823
+ document.head.appendChild(script);
824
+ });
825
+ }
826
+ async function loadPeerJS() {
827
+ if (window.Peer) {
828
+ return;
829
+ }
830
+ if (document.querySelector('script[src*="peerjs"]')) {
831
+ return new Promise((resolve) => {
832
+ const check = () => {
833
+ if (window.Peer) resolve();
834
+ else setTimeout(check, 50);
835
+ };
836
+ check();
837
+ });
838
+ }
839
+ const script = document.createElement("script");
840
+ script.src = "https://unpkg.com/peerjs@1.5.4/dist/peerjs.min.js";
841
+ script.async = true;
842
+ return new Promise((resolve, reject) => {
843
+ script.onload = () => resolve();
844
+ script.onerror = () => reject(new Error("Failed to load PeerJS"));
845
+ document.head.appendChild(script);
846
+ });
847
+ }
848
+ async function ensurePeerJS() {
849
+ if (!peerJSLoadPromise) {
850
+ peerJSLoadPromise = loadPeerJS();
851
+ }
852
+ await peerJSLoadPromise;
853
+ }
854
+
855
+ // src/multiplayer.ts
856
+ function getMultiplayer(config, game) {
857
+ const BoardgameMultiplayer = window.BoardgameMultiplayer;
858
+ const BoardgameAI = window.BoardgameAI;
859
+ if (config.isMultiplayer) {
860
+ return void 0;
861
+ }
862
+ const bots = {};
863
+ const botCount = config.botCount ?? 0;
864
+ if (botCount > 0 && game?.ai && BoardgameAI?.MCTSBot) {
865
+ for (let i = 1; i <= botCount; i++) {
866
+ bots[String(i)] = BoardgameAI.MCTSBot;
867
+ }
868
+ }
869
+ if (!BoardgameMultiplayer?.Local) {
870
+ return void 0;
871
+ }
872
+ return BoardgameMultiplayer.Local({
873
+ storageKey: `kossabos:bgio:${config.appId}`,
874
+ ...Object.keys(bots).length > 0 ? { bots } : {}
875
+ });
876
+ }
877
+ function generateMatchID() {
878
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
879
+ return Array.from(
880
+ { length: 6 },
881
+ () => chars[Math.floor(Math.random() * chars.length)]
882
+ ).join("");
883
+ }
884
+ export {
885
+ P2PDB,
886
+ P2PHost,
887
+ P2PTransport,
888
+ SettingsProvider,
889
+ cleanup,
890
+ createGameMount,
891
+ createMountFromExports,
892
+ createP2PTransport,
893
+ ensurePeerJS,
894
+ generateCredentials,
895
+ generateKeyPair,
896
+ generateMatchID,
897
+ getMultiplayer,
898
+ injectMountHelper,
899
+ mount,
900
+ setup,
901
+ useSettings
902
+ };
903
+ //# sourceMappingURL=index.js.map