@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/LICENSE +21 -0
- package/dist/index.d.ts +334 -0
- package/dist/index.js +903 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
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
|