@onceworlds/cli 0.9.4 → 0.10.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.
@@ -0,0 +1,1259 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // ../sdk/src/helpers/random.ts
6
+ function hashString(text) {
7
+ let h1 = 3735928559;
8
+ let h2 = 1103547991;
9
+ for (let i = 0; i < text.length; i++) {
10
+ const c = text.charCodeAt(i);
11
+ h1 = Math.imul(h1 ^ c, 2654435761);
12
+ h2 = Math.imul(h2 ^ c, 1597334677);
13
+ }
14
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
15
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
16
+ return (h2 ^ h1) >>> 0;
17
+ }
18
+ function createRandom(seed) {
19
+ const s = typeof seed === "number" && Number.isFinite(seed) ? Math.floor(seed) >>> 0 : hashString(String(seed));
20
+ let a = 2654435769;
21
+ let b = 608135816;
22
+ let c = 3084996962;
23
+ let d = s;
24
+ const next = () => {
25
+ a >>>= 0;
26
+ b >>>= 0;
27
+ c >>>= 0;
28
+ d >>>= 0;
29
+ let t2 = a + b | 0;
30
+ a = b ^ b >>> 9;
31
+ b = c + (c << 3) | 0;
32
+ c = c << 21 | c >>> 11;
33
+ d = d + 1 | 0;
34
+ t2 = t2 + d | 0;
35
+ c = c + t2 | 0;
36
+ return (t2 >>> 0) / 4294967296;
37
+ };
38
+ for (let i = 0; i < 12; i++) next();
39
+ const int = (n) => Number.isFinite(n) && n > 0 ? Math.floor(next() * Math.floor(n)) : 0;
40
+ return {
41
+ next,
42
+ int,
43
+ range: (min, max) => min + next() * (max - min),
44
+ chance: (p) => next() < p,
45
+ pick: (items) => Array.isArray(items) && items.length > 0 ? items[int(items.length)] : void 0,
46
+ shuffle(items) {
47
+ const out = Array.isArray(items) ? [...items] : [];
48
+ for (let i = out.length - 1; i > 0; i--) {
49
+ const j = int(i + 1);
50
+ [out[i], out[j]] = [out[j], out[i]];
51
+ }
52
+ return out;
53
+ }
54
+ };
55
+ }
56
+
57
+ // ../sdk/src/helpers/util.ts
58
+ var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
59
+ function jsonSize(value) {
60
+ try {
61
+ const text = JSON.stringify(value);
62
+ return text === void 0 ? 0 : text.length;
63
+ } catch {
64
+ return Infinity;
65
+ }
66
+ }
67
+ function copy(value) {
68
+ try {
69
+ return value === void 0 ? void 0 : JSON.parse(JSON.stringify(value));
70
+ } catch {
71
+ return void 0;
72
+ }
73
+ }
74
+ var said = /* @__PURE__ */ new Map();
75
+ function warn(key, message, level = "warn", error) {
76
+ const now = Date.now();
77
+ if (now - (said.get(key) ?? -Infinity) < 5e3) return;
78
+ said.set(key, now);
79
+ if (said.size > 500) said.delete(said.keys().next().value);
80
+ try {
81
+ const text = `[onceworlds] ${message}`;
82
+ if (level === "error") console.error(text, error);
83
+ else if (level === "debug") console.debug(text);
84
+ else console.warn(text);
85
+ } catch {
86
+ }
87
+ }
88
+ function guard(where, fn, fallback) {
89
+ try {
90
+ return fn();
91
+ } catch (error) {
92
+ warn(`threw:${where}`, `${where} threw (the helper carries on)`, "error", error);
93
+ return fallback;
94
+ }
95
+ }
96
+ var Bucket = class {
97
+ constructor(perSecond, burst, now) {
98
+ __publicField(this, "perSecond", perSecond);
99
+ __publicField(this, "burst", burst);
100
+ __publicField(this, "tokens");
101
+ __publicField(this, "last");
102
+ this.tokens = burst;
103
+ this.last = now;
104
+ }
105
+ take(now) {
106
+ this.tokens = Math.min(this.burst, this.tokens + Math.max(0, now - this.last) / 1e3 * this.perSecond);
107
+ this.last = now;
108
+ if (this.tokens < 1) return false;
109
+ this.tokens -= 1;
110
+ return true;
111
+ }
112
+ };
113
+ var Limits = class {
114
+ constructor(perSecond, burst) {
115
+ __publicField(this, "perSecond", perSecond);
116
+ __publicField(this, "burst", burst);
117
+ __publicField(this, "buckets", /* @__PURE__ */ new Map());
118
+ }
119
+ take(id, now) {
120
+ let bucket = this.buckets.get(id);
121
+ if (!bucket) {
122
+ if (this.buckets.size > 256) this.buckets.clear();
123
+ this.buckets.set(id, bucket = new Bucket(this.perSecond, this.burst, now));
124
+ }
125
+ return bucket.take(now);
126
+ }
127
+ };
128
+ function token() {
129
+ const words = new Uint32Array(2);
130
+ const crypto = globalThis.crypto;
131
+ if (crypto?.getRandomValues) crypto.getRandomValues(words);
132
+ else for (let i = 0; i < words.length; i++) words[i] = Math.floor(Math.random() * 4294967296);
133
+ return words[0].toString(36) + words[1].toString(36);
134
+ }
135
+ function numberFrom(value, settings, fallback) {
136
+ const v = typeof value === "string" ? settings[value] : value;
137
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : fallback;
138
+ }
139
+
140
+ // ../sdk/src/helpers/schema.ts
141
+ var schema = (check2, optional = false) => ({ check: check2, optional });
142
+ function toSchema(like) {
143
+ if (like === void 0 || like === null) return null;
144
+ if (typeof like === "function") {
145
+ return schema((value, path) => {
146
+ let verdict;
147
+ try {
148
+ verdict = like(value);
149
+ } catch {
150
+ return `${path} failed its check`;
151
+ }
152
+ if (verdict === false) return `${path} failed its check`;
153
+ return typeof verdict === "string" ? `${path}: ${verdict}` : null;
154
+ });
155
+ }
156
+ if (typeof like.check === "function") return like;
157
+ return t.object(like);
158
+ }
159
+ function check(like, value, path = "value") {
160
+ const s = toSchema(like);
161
+ if (!s) return null;
162
+ try {
163
+ return s.check(value, path);
164
+ } catch {
165
+ return `${path} could not be checked`;
166
+ }
167
+ }
168
+ var DANGEROUS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
169
+ var t = {
170
+ /** A finite number in [min, max] (default: any finite number). */
171
+ number(min = -1e15, max = 1e15) {
172
+ return schema((v, path) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max ? null : `${path} must be a number from ${min} to ${max}`);
173
+ },
174
+ /** A whole number in [min, max]. */
175
+ int(min = -2147483648, max = 2147483647) {
176
+ return schema((v, path) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max ? null : `${path} must be a whole number from ${min} to ${max}`);
177
+ },
178
+ /** A string of at most `max` characters (default 200), matching `pattern` when given. */
179
+ string(max = 200, pattern) {
180
+ return schema((v, path) => {
181
+ if (typeof v !== "string" || v.length > max) return `${path} must be text of at most ${max} characters`;
182
+ return pattern && !pattern.test(v) ? `${path} is not in the right form` : null;
183
+ });
184
+ },
185
+ boolean() {
186
+ return schema((v, path) => typeof v === "boolean" ? null : `${path} must be true or false`);
187
+ },
188
+ /** One of these exact values (strings, numbers, booleans or null). */
189
+ oneOf(...values) {
190
+ return schema((v, path) => values.includes(v) ? null : `${path} must be one of ${values.map((x) => JSON.stringify(x)).join(", ")}`);
191
+ },
192
+ /** A player id (or a bot's seat id): text of up to 128 characters. Whether they are in the room is for `validate` to say. */
193
+ id() {
194
+ return schema((v, path) => typeof v === "string" && v.length > 0 && v.length <= 128 ? null : `${path} must be a player id`);
195
+ },
196
+ /** A list of at most `max` items (default 64), each passing `item`. */
197
+ array(item, max = 64) {
198
+ const inner = toSchema(item);
199
+ return schema((v, path) => {
200
+ if (!Array.isArray(v) || v.length > max) return `${path} must be a list of at most ${max}`;
201
+ for (let i = 0; i < v.length; i++) {
202
+ const problem = inner.check(v[i], `${path}[${i}]`);
203
+ if (problem) return problem;
204
+ }
205
+ return null;
206
+ });
207
+ },
208
+ /** An object with exactly these fields (unknown fields are refused; t.optional ones may be left out). */
209
+ object(shape) {
210
+ const fields = Object.entries(shape).map(([key, like]) => [key, toSchema(like)]);
211
+ const known = new Set(fields.map(([key]) => key));
212
+ return schema((v, path) => {
213
+ if (!isObject(v)) return `${path} must be an object`;
214
+ for (const key of Object.keys(v)) if (!known.has(key) || DANGEROUS.has(key)) return `${path} has an unknown field '${key.slice(0, 40)}'`;
215
+ for (const [key, inner] of fields) {
216
+ if (!Object.hasOwn(v, key) || v[key] === void 0) {
217
+ if (inner.optional) continue;
218
+ return `${path}.${key} is missing`;
219
+ }
220
+ const problem = inner.check(v[key], `${path}.${key}`);
221
+ if (problem) return problem;
222
+ }
223
+ return null;
224
+ });
225
+ },
226
+ /** An object used as a map: at most `maxKeys` keys (default 64) of up to 128 characters, each value passing `value`. */
227
+ record(value, maxKeys = 64) {
228
+ const inner = toSchema(value);
229
+ return schema((v, path) => {
230
+ if (!isObject(v)) return `${path} must be an object`;
231
+ const keys = Object.keys(v);
232
+ if (keys.length > maxKeys) return `${path} has more than ${maxKeys} keys`;
233
+ for (const key of keys) {
234
+ if (key.length > 128 || DANGEROUS.has(key)) return `${path} has a bad key`;
235
+ const problem = inner.check(v[key], `${path}.${key}`);
236
+ if (problem) return problem;
237
+ }
238
+ return null;
239
+ });
240
+ },
241
+ /** The field may be left out (or undefined). */
242
+ optional(inner) {
243
+ const s = toSchema(inner);
244
+ return schema((v, path) => v === void 0 ? null : s.check(v, path), true);
245
+ },
246
+ /** null, or a value passing `inner`. */
247
+ nullable(inner) {
248
+ const s = toSchema(inner);
249
+ return schema((v, path) => v === null ? null : s.check(v, path), s.optional);
250
+ },
251
+ /** Any plain data of up to `maxBytes` as JSON (default 1024). Prefer a real shape: this only bounds the size. */
252
+ any(maxBytes = 1024) {
253
+ return schema((v, path) => jsonSize(v) <= maxBytes ? null : `${path} is bigger than ${maxBytes} bytes`);
254
+ }
255
+ };
256
+
257
+ // ../sdk/src/helpers/host.ts
258
+ var MESSAGE_TAG = "$ow";
259
+ var NAME_RE = /^[A-Za-z0-9_.:-]{1,48}$/;
260
+ var STATE_NAME_RE = /^[A-Za-z0-9_.:-]{1,59}$/;
261
+ var BOT_ID_RE = /^bot:[A-Za-z0-9_.-]{1,24}$/;
262
+ var CLAIM_TIMEOUT_MS = 5e3;
263
+ var STATE_MAX_BYTES = 15e3;
264
+ function platformNow() {
265
+ try {
266
+ const now = globalThis.onceworlds?.now?.();
267
+ if (typeof now === "number" && Number.isFinite(now)) return now;
268
+ } catch {
269
+ }
270
+ return Date.now();
271
+ }
272
+ var callerOf = (player) => ({ id: player.id, name: player.name, team: player.team ?? 0, bot: false });
273
+ function createHost(room, options = {}) {
274
+ const rpcs = /* @__PURE__ */ new Map();
275
+ const claims = /* @__PURE__ */ new Map();
276
+ const open = /* @__PURE__ */ new Map();
277
+ const seen = /* @__PURE__ */ new Set();
278
+ const waiting = /* @__PURE__ */ new Map();
279
+ const offs = [];
280
+ let disposed = false;
281
+ const now = () => {
282
+ try {
283
+ return options.now ? options.now() : platformNow();
284
+ } catch {
285
+ return Date.now();
286
+ }
287
+ };
288
+ const reject = (playerId, reason) => {
289
+ if (options.onReject) guard("onReject", () => options.onReject(playerId, reason), void 0);
290
+ else warn(`reject:${playerId}`, `refused a message from ${playerId}: ${reason}`, "debug");
291
+ };
292
+ const mayCall = (from, spectators) => {
293
+ if (from.bot || spectators || room.match.phase === "lobby") return true;
294
+ return from.id === room.host || room.isParticipant(from.id);
295
+ };
296
+ function runRpc(name, entry, from, raw, info) {
297
+ if (!mayCall(from, entry.def.spectators)) return reject(from.id, `'${name}' from a player who is only watching`), false;
298
+ if (!entry.limits.take(from.id, now())) return reject(from.id, `'${name}' called too often`), false;
299
+ if (jsonSize(raw) > (entry.def.maxBytes ?? 2048)) return reject(from.id, `'${name}' arguments are too big`), false;
300
+ const args = raw === void 0 && entry.schema ? {} : raw;
301
+ if (entry.schema) {
302
+ const problem = entry.schema.check(args, `'${name}' args`);
303
+ if (problem) return reject(from.id, problem), false;
304
+ } else if (args !== void 0 && args !== null && !(isObject(args) && Object.keys(args).length === 0)) {
305
+ return reject(from.id, `'${name}' takes no arguments`), false;
306
+ }
307
+ const verdict = guard(`rpc '${name}' validate`, () => entry.def.validate?.(from, args, info), false);
308
+ if (verdict === false || typeof verdict === "string") return reject(from.id, typeof verdict === "string" ? verdict : `'${name}' refused`), false;
309
+ guard(`rpc '${name}' run`, () => entry.def.run(from, args, info), void 0);
310
+ return true;
311
+ }
312
+ function rpc(name, def) {
313
+ if (typeof name !== "string" || !NAME_RE.test(name) || !def || typeof def.run !== "function") {
314
+ warn(`rpc-def:${String(name)}`, `host.rpc('${String(name)}', { run(from, args) {...} }) needs a name of 1-48 letters, digits or _.:- and a run function`, "error");
315
+ return () => false;
316
+ }
317
+ if (rpcs.has(name)) warn(`rpc-twice:${name}`, `host.rpc('${name}') was defined twice: the second replaces the first. Define each once, where the game starts.`);
318
+ const rate = def.rate && def.rate > 0 ? def.rate : 10;
319
+ const entry = { def, schema: toSchema(def.args), limits: new Limits(rate, Math.max(4, rate * 2)) };
320
+ rpcs.set(name, entry);
321
+ return (args) => {
322
+ if (disposed || room.closed) return false;
323
+ const wire = args === void 0 ? void 0 : copy(args);
324
+ if (args !== void 0 && wire === void 0) return warn(`rpc-args:${name}`, `rpc '${name}': arguments must be plain data`), false;
325
+ if (entry.schema) {
326
+ const problem = check(entry.schema, wire === void 0 ? {} : wire, `'${name}' args`);
327
+ if (problem) return warn(`rpc-args:${name}`, `rpc '${name}' not sent: ${problem}`), false;
328
+ }
329
+ if (room.isHost) {
330
+ const me = room.players.get(room.me.id) ?? room.me;
331
+ return runRpc(name, entry, callerOf(me), wire, { at: now(), ...room.running ? { matchTime: room.matchNow() } : {} });
332
+ }
333
+ room.send({ [MESSAGE_TAG]: "rpc", n: name, ...wire === void 0 ? {} : { a: wire } }, { to: room.host });
334
+ return true;
335
+ };
336
+ }
337
+ function claim(name, def) {
338
+ if (typeof name !== "string" || !NAME_RE.test(name) || !def || typeof def.apply !== "function") {
339
+ warn(`claim-def:${String(name)}`, `host.claim('${String(name)}', { apply(winner, rivals) {...} }) needs a name of 1-48 letters, digits or _.:- and an apply function`, "error");
340
+ return async () => "rejected";
341
+ }
342
+ const rate = def.rate && def.rate > 0 ? def.rate : 10;
343
+ const entry = { def, schema: toSchema(def.args), limits: new Limits(rate, Math.max(4, rate * 2)) };
344
+ claims.set(name, entry);
345
+ return (args) => {
346
+ if (disposed || room.closed) return Promise.resolve("rejected");
347
+ const wire = copy(args === void 0 ? {} : args);
348
+ const problem = wire === void 0 ? "arguments must be plain data" : entry.schema ? check(entry.schema, wire, `'${name}' args`) : null;
349
+ if (problem) {
350
+ warn(`claim-args:${name}`, `claim '${name}' not sent: ${problem}`);
351
+ return Promise.resolve("rejected");
352
+ }
353
+ const id = token();
354
+ const message = { [MESSAGE_TAG]: "claim", n: name, i: id, a: wire };
355
+ return new Promise((resolve) => {
356
+ const timer = setTimeout(() => settleMine(id, "timeout"), CLAIM_TIMEOUT_MS);
357
+ waiting.set(id, { resolve, message, sentTo: [room.host], timer });
358
+ if (room.isHost) receiveClaim(callerOf(room.players.get(room.me.id) ?? room.me), name, id, wire, { at: now(), ...room.running ? { matchTime: room.matchNow() } : {} });
359
+ else room.send(message, { to: room.host });
360
+ });
361
+ };
362
+ }
363
+ function settleMine(id, result) {
364
+ const w = waiting.get(id);
365
+ if (!w) return;
366
+ waiting.delete(id);
367
+ clearTimeout(w.timer);
368
+ w.resolve(result);
369
+ }
370
+ function answer(playerId, id, result) {
371
+ if (playerId === room.me.id) settleMine(id, result);
372
+ else if (!playerId.startsWith("bot:")) room.send({ [MESSAGE_TAG]: "ans", i: id, r: result }, { to: playerId });
373
+ }
374
+ function receiveClaim(from, name, id, raw, info) {
375
+ const entry = claims.get(name);
376
+ if (!entry) return false;
377
+ const seenKey = `${from.id}:${id}`;
378
+ if (seen.has(seenKey)) return false;
379
+ seen.add(seenKey);
380
+ if (seen.size > 512) seen.delete(seen.values().next().value);
381
+ const refuse = (reason) => (reject(from.id, reason), answer(from.id, id, "rejected"), false);
382
+ if (!mayCall(from, entry.def.spectators)) return refuse(`claim '${name}' from a player who is only watching`);
383
+ if (!entry.limits.take(from.id, now())) return refuse(`claim '${name}' made too often`);
384
+ if (jsonSize(raw) > (entry.def.maxBytes ?? 2048)) return refuse(`claim '${name}' arguments are too big`);
385
+ const args = raw === void 0 ? {} : raw;
386
+ const problem = entry.schema?.check(args, `'${name}' args`);
387
+ if (problem) return refuse(problem);
388
+ const made = { id, from, args, at: info.at, ...info.matchTime !== void 0 ? { matchTime: info.matchTime } : {} };
389
+ const keyed = entry.def.key ? guard(`claim '${name}' key`, () => String(entry.def.key(args)), "") : "";
390
+ const key = keyed ? `${name}|${keyed}` : `${name}|#${seenKey}`;
391
+ const group = open.get(key);
392
+ if (group) {
393
+ group.claims.push(made);
394
+ return true;
395
+ }
396
+ const window = Math.max(0, Math.min(5e3, entry.def.window ?? (keyed ? 150 : 0)));
397
+ open.set(key, { entry, claims: [made], timer: setTimeout(() => settle(key), window) });
398
+ return true;
399
+ }
400
+ function settle(key) {
401
+ const group = open.get(key);
402
+ if (!group) return;
403
+ open.delete(key);
404
+ if (!room.isHost || disposed) return;
405
+ const sorted = group.claims.sort((a, b) => a.at - b.at || (a.from.id < b.from.id ? -1 : a.from.id > b.from.id ? 1 : 0));
406
+ let winner;
407
+ const rivals = [];
408
+ for (const c of sorted) {
409
+ if (winner) {
410
+ rivals.push(c);
411
+ continue;
412
+ }
413
+ const verdict = guard("claim validate", () => group.entry.def.validate?.(c), false);
414
+ if (verdict === false || typeof verdict === "string") {
415
+ reject(c.from.id, typeof verdict === "string" ? verdict : "claim refused");
416
+ answer(c.from.id, c.id, "rejected");
417
+ } else winner = c;
418
+ }
419
+ if (!winner) return;
420
+ guard("claim apply", () => group.entry.def.apply(winner, rivals), void 0);
421
+ for (const rival of rivals) answer(rival.from.id, rival.id, "lost");
422
+ answer(winner.from.id, winner.id, "won");
423
+ }
424
+ offs.push(
425
+ room.on("message", (data, player, at, matchTime) => {
426
+ if (disposed || !isObject(data) || typeof data[MESSAGE_TAG] !== "string") return;
427
+ const kind = data[MESSAGE_TAG];
428
+ const info = { at: typeof at === "number" ? at : now(), ...typeof matchTime === "number" ? { matchTime } : {} };
429
+ if (kind === "ans") {
430
+ const id = data.i;
431
+ if (typeof id !== "string") return;
432
+ const w = waiting.get(id);
433
+ if (!w) return;
434
+ if (!w.sentTo.includes(player.id)) return reject(player.id, "an answer to a claim that was not sent to them");
435
+ if (data.r === "won" || data.r === "lost" || data.r === "rejected") settleMine(id, data.r);
436
+ return;
437
+ }
438
+ if (!room.isHost || typeof data.n !== "string" || data.n.length > 48) return;
439
+ if (!room.players.has(player.id)) return reject(player.id, "a message from somebody who is not in the room");
440
+ const from = callerOf(player);
441
+ if (kind === "rpc") {
442
+ const entry = rpcs.get(data.n);
443
+ if (entry) runRpc(data.n, entry, from, data.a, info);
444
+ } else if (kind === "claim") {
445
+ if (typeof data.i !== "string" || data.i.length > 32) return reject(player.id, "a claim without an id");
446
+ if (claims.has(data.n)) receiveClaim(from, data.n, data.i, data.a, info);
447
+ }
448
+ })
449
+ );
450
+ offs.push(
451
+ room.on("host", (hostId) => {
452
+ for (const [id, w] of waiting) {
453
+ if (w.sentTo[w.sentTo.length - 1] === hostId) continue;
454
+ w.sentTo.push(hostId);
455
+ const message = w.message;
456
+ if (hostId === room.me.id && room.isHost) receiveClaim(callerOf(room.players.get(room.me.id) ?? room.me), message.n, id, message.a, { at: now() });
457
+ else room.send(w.message, { to: hostId });
458
+ }
459
+ })
460
+ );
461
+ offs.push(room.on("close", () => dispose()));
462
+ function state(name, opts = {}) {
463
+ const ok = typeof name === "string" && STATE_NAME_RE.test(name);
464
+ if (!ok) warn(`state-name:${String(name)}`, `host.state('${String(name)}'): a name is 1-59 letters, digits or _.:-`, "error");
465
+ const key = `host:${ok ? name : "invalid"}`;
466
+ const schema2 = toSchema(opts.schema);
467
+ let lastRaw = void 0;
468
+ let lastGood = void 0;
469
+ const initial = () => copy(opts.initial);
470
+ const get = () => {
471
+ const raw = room.state[key];
472
+ if (raw === void 0 || raw === null) return lastGood ?? initial();
473
+ if (raw === lastRaw) return lastGood ?? initial();
474
+ lastRaw = raw;
475
+ const problem = schema2 ? check(schema2, raw, key) : null;
476
+ if (problem) {
477
+ warn(`state-bad:${key}`, `ignored ${key} from the host: ${problem}`);
478
+ return lastGood ?? initial();
479
+ }
480
+ lastGood = raw;
481
+ return lastGood;
482
+ };
483
+ const set = (value) => {
484
+ if (!ok || disposed || room.closed) return false;
485
+ if (!room.isHost) return warn(`state-host:${key}`, `${key}.set() did nothing: only the host writes it (check host.isHost)`), false;
486
+ const clean = value === void 0 ? null : copy(value);
487
+ if (value !== void 0 && value !== null && clean === void 0) return warn(`state-data:${key}`, `${key}.set(): the value must be plain data`), false;
488
+ if (schema2 && clean !== null) {
489
+ const problem = check(schema2, clean, key);
490
+ if (problem) return warn(`state-schema:${key}`, `${key}.set() did nothing: ${problem}`), false;
491
+ }
492
+ if (jsonSize(clean) > STATE_MAX_BYTES) return warn(`state-size:${key}`, `${key}.set() did nothing: the value is over ${STATE_MAX_BYTES} bytes (split it over several states)`), false;
493
+ room.setState(key, clean);
494
+ return true;
495
+ };
496
+ return {
497
+ key,
498
+ get: () => guard(`${key}.get`, get, initial()),
499
+ set: (value) => guard(`${key}.set`, () => set(value), false),
500
+ update(fn) {
501
+ if (!room.isHost) return warn(`state-host:${key}`, `${key}.update() did nothing: only the host writes it (check host.isHost)`), false;
502
+ const draft = copy(get());
503
+ const out = guard(`${key}.update`, () => fn(draft), void 0);
504
+ return set(out === void 0 ? draft : out);
505
+ },
506
+ on(listener) {
507
+ return room.on("state", (k) => {
508
+ if (k === key) guard(`${key} listener`, () => listener(get()), void 0);
509
+ });
510
+ }
511
+ };
512
+ }
513
+ function callAs(bot, name, args) {
514
+ const id = typeof bot === "string" ? bot : bot?.id;
515
+ if (!room.isHost || disposed || typeof id !== "string" || !BOT_ID_RE.test(id)) return false;
516
+ const from = { id, name: typeof bot === "object" && typeof bot.name === "string" ? bot.name : id, team: 0, bot: true };
517
+ const info = { at: now(), ...room.running ? { matchTime: room.matchNow() } : {} };
518
+ const wire = args === void 0 ? void 0 : copy(args);
519
+ const entry = rpcs.get(name);
520
+ if (entry) return runRpc(name, entry, from, wire, info);
521
+ if (claims.has(name)) return receiveClaim(from, name, token(), wire, info);
522
+ warn(`callAs:${name}`, `callAs: no rpc or claim named '${name}'`);
523
+ return false;
524
+ }
525
+ function dispose() {
526
+ if (disposed) return;
527
+ disposed = true;
528
+ for (const off of offs) off();
529
+ for (const group of open.values()) clearTimeout(group.timer);
530
+ open.clear();
531
+ for (const id of [...waiting.keys()]) settleMine(id, "timeout");
532
+ }
533
+ return {
534
+ get isHost() {
535
+ return room.isHost;
536
+ },
537
+ state,
538
+ rpc,
539
+ claim,
540
+ callAs: (bot, name, args) => guard("callAs", () => callAs(bot, name, args), false),
541
+ random: (name) => createRandom(`${room.match.seed ?? 0}:${String(name)}`),
542
+ dispose
543
+ };
544
+ }
545
+
546
+ // ../sdk/src/helpers/match.ts
547
+ var MATCH_KEY = "host:ow.match";
548
+ var RECORD_PHASES = ["intro", "round", "roundEnd", "results"];
549
+ var MAX_SEATS = 64;
550
+ var RECORD_MAX_BYTES = 15e3;
551
+ var ADOPT_MS = 600;
552
+ var str = (v, max) => typeof v === "string" && v.length <= max;
553
+ var num = (v, min = -1e12, max = 1e12) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max;
554
+ var ids = (v) => Array.isArray(v) && v.length <= MAX_SEATS && v.every((id) => str(id, 128));
555
+ function numbers(v) {
556
+ if (!isObject(v) || Object.keys(v).length > MAX_SEATS * 2) return null;
557
+ for (const [key, value] of Object.entries(v)) if (key.length > 128 || !num(value)) return null;
558
+ return v;
559
+ }
560
+ var standingSchema = t.array(t.object({ id: t.id(), name: t.string(64), points: t.number(), place: t.int(1, 1e3), bot: t.boolean() }), MAX_SEATS);
561
+ function readRecord(raw, dataSchema) {
562
+ if (!isObject(raw) || raw.v !== 1) return null;
563
+ const r = raw;
564
+ if (!str(r.mid, 200) || !num(r.n, 0) || !str(r.by, 128) || typeof r.phase !== "string" || !RECORD_PHASES.includes(r.phase)) return null;
565
+ if (!num(r.round, 0, 1e5) || !num(r.rounds, 0, 1e5) || !num(r.until, 0) || !num(r.started, 0) || typeof r.done !== "boolean") return null;
566
+ if (!isObject(r.names) || Object.keys(r.names).length > MAX_SEATS * 2 || !Object.values(r.names).every((name) => str(name, 64))) return null;
567
+ if (!Array.isArray(r.bots) || r.bots.length > MAX_SEATS || !r.bots.every((b) => isObject(b) && str(b.id, 32) && b.id.startsWith("bot:") && str(b.name, 32) && num(b.skill, 0, 1))) return null;
568
+ const scores = numbers(r.scores);
569
+ if (!scores) return null;
570
+ if (r.last !== null) {
571
+ if (!isObject(r.last) || !Array.isArray(r.last.ranking) || r.last.ranking.length > MAX_SEATS || !r.last.ranking.every(ids) || !numbers(r.last.points)) return null;
572
+ }
573
+ if (r.turn !== null) {
574
+ const tr = r.turn;
575
+ if (!isObject(tr) || !num(tr.n, 0, 1e6) || !ids(tr.order) || !num(tr.i, 0, MAX_SEATS) || !str(tr.id, 128) || !num(tr.until, 0)) return null;
576
+ }
577
+ if (r.results !== null && check(standingSchema, r.results)) return null;
578
+ let data = r.data ?? null;
579
+ if (dataSchema && data !== null && check(dataSchema, data, "match.data")) {
580
+ warn("match-data", `ignored match.data from the host: ${check(dataSchema, data, "match.data")}`);
581
+ data = null;
582
+ }
583
+ return { ...r, data };
584
+ }
585
+ function rankingOf(outcome, known) {
586
+ const ok = new Set(known);
587
+ const seen = /* @__PURE__ */ new Set();
588
+ const clean = (list) => {
589
+ const out = [];
590
+ for (const id of list) if (typeof id === "string" && ok.has(id) && !seen.has(id)) seen.add(id), out.push(id);
591
+ return out;
592
+ };
593
+ if (!outcome || typeof outcome !== "object") return [];
594
+ if (Array.isArray(outcome)) {
595
+ if (outcome.length === 0) return [];
596
+ if (outcome.every((item) => typeof item === "string")) {
597
+ const winners = clean(outcome);
598
+ return winners.length ? [winners] : [];
599
+ }
600
+ return outcome.filter(Array.isArray).map((group) => clean(group)).filter((group) => group.length > 0);
601
+ }
602
+ const entries = Object.entries(outcome).filter((e) => ok.has(e[0]) && num(e[1]));
603
+ entries.sort((a, b) => b[1] - a[1]);
604
+ const groups = [];
605
+ let previous;
606
+ for (const [id, value] of entries) {
607
+ if (value === previous) groups[groups.length - 1].push(id);
608
+ else groups.push([id]);
609
+ previous = value;
610
+ }
611
+ return groups;
612
+ }
613
+ function pointsOf(outcome, ranking, known, table) {
614
+ const out = {};
615
+ if (outcome && typeof outcome === "object" && !Array.isArray(outcome)) {
616
+ const ok = new Set(known);
617
+ for (const [id, value] of Object.entries(outcome)) if (ok.has(id) && num(value, -1e6, 1e6)) out[id] = value;
618
+ return out;
619
+ }
620
+ let place = 0;
621
+ for (const group of ranking) {
622
+ const points = table[place] ?? 0;
623
+ for (const id of group) out[id] = points;
624
+ place += group.length;
625
+ }
626
+ return out;
627
+ }
628
+ function standingsOf(seats, scores, placings) {
629
+ const rows = seats.map((s) => ({ id: s.id, name: s.name.slice(0, 64), points: scores[s.id] ?? 0, place: 0, bot: s.bot }));
630
+ if (placings && placings.length > 0) {
631
+ const placeOf = /* @__PURE__ */ new Map();
632
+ let place = 1;
633
+ for (const group of placings) {
634
+ for (const id of group) placeOf.set(id, place);
635
+ place += group.length;
636
+ }
637
+ for (const row of rows) row.place = placeOf.get(row.id) ?? place;
638
+ rows.sort((a, b) => a.place - b.place || b.points - a.points);
639
+ return rows;
640
+ }
641
+ rows.sort((a, b) => b.points - a.points);
642
+ rows.forEach((row, i) => row.place = i > 0 && rows[i - 1].points === row.points ? rows[i - 1].place : i + 1);
643
+ return rows;
644
+ }
645
+ var BOT_NAMES = ["Mox", "Juno", "Pip", "Rook", "Vega", "Tilly", "Bram", "Nix", "Sable", "Otto", "Wren", "Kit", "Lumo", "Dash", "Fern", "Zed"];
646
+ function platformNow2() {
647
+ try {
648
+ const now = globalThis.onceworlds?.now?.();
649
+ if (typeof now === "number" && Number.isFinite(now)) return now;
650
+ } catch {
651
+ }
652
+ return Date.now();
653
+ }
654
+ function createMatch(room, rules = {}) {
655
+ const listeners = /* @__PURE__ */ new Map();
656
+ const host = createHost(room);
657
+ const offs = [];
658
+ const dataSchema = rules.data === void 0 ? void 0 : toSchema(rules.data) ?? void 0;
659
+ let disposed = false;
660
+ let mem = null;
661
+ let cache = { raw: void 0, rec: null };
662
+ let lastGood = null;
663
+ let adoptSince = 0;
664
+ let confirmed = void 0;
665
+ let lastTick = null;
666
+ let ended = "";
667
+ let pendingEnd = null;
668
+ let turnEnd = 0;
669
+ let awaySince = /* @__PURE__ */ new Map();
670
+ let tickQueued = false;
671
+ let shownPhase = "lobby";
672
+ let shownRound = "";
673
+ let shownTurn = "";
674
+ let firstRecord = true;
675
+ let banked = "";
676
+ let reported = "";
677
+ let botFill = 0;
678
+ let botNames = BOT_NAMES;
679
+ let botSkill = [0.3, 0.7];
680
+ const me = () => room.me.id;
681
+ const settings = () => room.settings ?? {};
682
+ const matchMs = () => room.matchNow();
683
+ const emit = (event, ...args) => {
684
+ for (const listener of [...listeners.get(event) ?? []]) guard(`match '${event}' listener`, () => listener(...args), void 0);
685
+ };
686
+ function record() {
687
+ const raw = room.state[MATCH_KEY];
688
+ if (raw !== cache.raw) cache = { raw, rec: readRecord(raw, dataSchema) };
689
+ return cache.rec;
690
+ }
691
+ function current() {
692
+ const m = room.match;
693
+ if (room.isHost && mem && m.phase === "playing" && mem.mid === m.id && mem.by === me()) return mem;
694
+ const rec = record();
695
+ if (m.phase !== "lobby") {
696
+ if (rec && rec.mid === m.id) return lastGood = rec;
697
+ return room.state[MATCH_KEY] !== void 0 && lastGood?.mid === m.id ? lastGood : null;
698
+ }
699
+ return rec && rec.n === m.n && rec.phase === "results" ? rec : null;
700
+ }
701
+ function seatsOf(rec) {
702
+ const m = room.match;
703
+ if (m.phase === "lobby" && !rec) return [];
704
+ const humans = m.phase !== "lobby" ? m.participants ?? [] : (rec?.results ?? []).filter((s) => !s.bot).map((s) => s.id);
705
+ const seats = humans.map((id, index) => {
706
+ const p = room.players.get(id);
707
+ return { id, name: p?.name ?? rec?.names[id] ?? "Player", bot: false, me: id === me(), index, team: p?.team ?? 0, away: p?.connected === false, idle: !!p?.idle, left: !p, skill: 0 };
708
+ });
709
+ for (const b of rec?.bots ?? []) seats.push({ id: b.id, name: b.name, bot: true, me: false, index: seats.length, team: 0, away: false, idle: false, left: false, skill: b.skill });
710
+ return seats;
711
+ }
712
+ function phaseOf(rec) {
713
+ const m = room.match;
714
+ if (m.phase === "lobby") return "lobby";
715
+ if (m.phase === "starting") return "countdown";
716
+ return rec ? rec.phase : "countdown";
717
+ }
718
+ const total = () => Math.floor(numberFrom(rules.rounds ?? 3, settings(), 3));
719
+ function write(patch) {
720
+ if (!mem) return false;
721
+ const next = { ...mem, ...patch, by: me() };
722
+ if (jsonSize(next) > RECORD_MAX_BYTES) {
723
+ warn("match-size", `the match record is over ${RECORD_MAX_BYTES} bytes (match.data is too big): keep big things in host.state() instead`, "error");
724
+ if (!("data" in patch)) return false;
725
+ next.data = mem.data;
726
+ }
727
+ mem = next;
728
+ room.setState(MATCH_KEY, mem);
729
+ requestSync();
730
+ return true;
731
+ }
732
+ function acting() {
733
+ const m = room.match;
734
+ return !disposed && room.isHost && m.phase === "playing" && room.running && !!mem && mem.mid === m.id && mem.by === me() && adoptSince === 0;
735
+ }
736
+ function requestTick() {
737
+ if (tickQueued) return;
738
+ tickQueued = true;
739
+ queueMicrotask(() => {
740
+ tickQueued = false;
741
+ tick();
742
+ });
743
+ }
744
+ function tick() {
745
+ if (disposed) return;
746
+ const m = room.match;
747
+ if (!room.isHost || m.phase !== "playing" || !room.running) {
748
+ adoptSince = 0;
749
+ lastTick = null;
750
+ return;
751
+ }
752
+ let rec = record();
753
+ if (!rec && room.state[MATCH_KEY] !== void 0 && lastGood?.mid === m.id) rec = lastGood;
754
+ if (!rec || rec.mid !== m.id) {
755
+ if (mem && mem.mid === m.id && mem.by === me()) rec = mem;
756
+ else return beginMatch(m);
757
+ }
758
+ if (!mem || mem.mid !== rec.mid || rec.by !== me()) {
759
+ const now = Date.now();
760
+ adoptSince || (adoptSince = now);
761
+ mem = rec;
762
+ if (now - adoptSince < ADOPT_MS) return;
763
+ adoptSince = 0;
764
+ write({});
765
+ awaySince = /* @__PURE__ */ new Map();
766
+ guard("match resume", () => rules.resume?.(api), void 0);
767
+ requestSync();
768
+ }
769
+ const raw = room.state[MATCH_KEY];
770
+ if (raw !== mem && raw !== confirmed) {
771
+ if (JSON.stringify(raw) === JSON.stringify(mem)) confirmed = raw;
772
+ else write({});
773
+ }
774
+ step();
775
+ }
776
+ function beginMatch(m) {
777
+ const humans = m.participants ?? [];
778
+ const count = Math.max(0, Math.min(30 - humans.length, botFill - humans.length));
779
+ const rng = createRandom(`${m.seed ?? 0}:ow.bots`);
780
+ const names = rng.shuffle(botNames.length ? botNames : BOT_NAMES);
781
+ const bots = Array.from({ length: count }, (_, i) => {
782
+ const skill = typeof botSkill === "number" ? botSkill : botSkill[0] + rng.next() * (botSkill[1] - botSkill[0]);
783
+ const base = String(names[i % names.length]).slice(0, 24);
784
+ return { id: `bot:${i + 1}`, name: i < names.length ? base : `${base} ${Math.floor(i / names.length) + 1}`, skill: Math.round(Math.min(1, Math.max(0, skill)) * 100) / 100 };
785
+ });
786
+ mem = {
787
+ v: 1,
788
+ mid: m.id ?? "",
789
+ n: m.n,
790
+ by: me(),
791
+ phase: "intro",
792
+ round: 0,
793
+ rounds: total(),
794
+ until: 0,
795
+ started: 0,
796
+ names: {},
797
+ bots,
798
+ scores: {},
799
+ last: null,
800
+ turn: null,
801
+ done: false,
802
+ data: null,
803
+ results: null
804
+ };
805
+ ended = "";
806
+ adoptSince = 0;
807
+ beginRound(1);
808
+ }
809
+ function beginRound(round) {
810
+ if (!mem) return;
811
+ const humans = room.match.participants ?? [];
812
+ const bots = mem.bots.slice(0, Math.max(0, Math.min(mem.bots.length, botFill - humans.length)));
813
+ const names = { ...mem.names };
814
+ for (const id of humans) {
815
+ const p = room.players.get(id);
816
+ if (p) names[id] = p.name.slice(0, 64);
817
+ }
818
+ const scores = { ...mem.scores };
819
+ for (const id of [...humans, ...bots.map((b) => b.id)]) scores[id] ?? (scores[id] = 0);
820
+ const intro = Math.max(0, (rules.introSeconds ?? 0) * 1e3);
821
+ const patch = { phase: "intro", round, until: matchMs() + intro, last: null, turn: null, data: null, bots, names, scores };
822
+ if (intro > 0) write(patch);
823
+ else {
824
+ mem = { ...mem, ...patch };
825
+ startPlay();
826
+ }
827
+ }
828
+ function startPlay() {
829
+ if (!mem) return;
830
+ const now = matchMs();
831
+ pendingEnd = null;
832
+ turnEnd = 0;
833
+ const secs = numberFrom(rules.roundSeconds ?? 0, settings(), 0);
834
+ mem = { ...mem, phase: "round", started: now, until: secs > 0 ? now + secs * 1e3 : 0, data: null };
835
+ const out = guard("startRound", () => rules.startRound?.(api), void 0);
836
+ const data = out === void 0 ? mem.data : copy(out) ?? null;
837
+ write({ phase: "round", data, turn: rules.turns ? firstTurn(now) : null });
838
+ if (mem.turn) beginTurn();
839
+ }
840
+ function step() {
841
+ if (!mem) return;
842
+ const now = matchMs();
843
+ const dt = lastTick === null ? 0 : Math.max(0, (now - lastTick) / 1e3);
844
+ lastTick = now;
845
+ switch (mem.phase) {
846
+ case "intro":
847
+ if (now >= mem.until) startPlay();
848
+ return;
849
+ case "round": {
850
+ if (pendingEnd) return endRoundNow(pendingEnd);
851
+ guard("onTick", () => rules.onTick?.(api, dt), void 0);
852
+ if (pendingEnd) return endRoundNow(pendingEnd);
853
+ if (mem.turn) tickTurn(now);
854
+ if (mem.phase === "round" && pendingEnd) return endRoundNow(pendingEnd);
855
+ if (mem.phase === "round" && mem.until > 0 && now >= mem.until) endRoundNow({ outcome: void 0, given: false });
856
+ return;
857
+ }
858
+ case "roundEnd":
859
+ if (now >= mem.until) {
860
+ if (mem.done) finish();
861
+ else beginRound(mem.round + 1);
862
+ }
863
+ return;
864
+ case "results":
865
+ if (mem.until > 0 && now >= mem.until && ended !== mem.mid) {
866
+ ended = mem.mid;
867
+ room.endMatch();
868
+ }
869
+ return;
870
+ }
871
+ }
872
+ function endRoundNow(end) {
873
+ pendingEnd = null;
874
+ if (!mem || mem.phase !== "round") return;
875
+ const seats = seatsOf(mem);
876
+ const known = seats.map((s) => s.id);
877
+ const outcome = end.given ? end.outcome : guard("score", () => rules.score?.(api), void 0);
878
+ const ranking = rankingOf(outcome, known);
879
+ const points = pointsOf(outcome, ranking, known, rules.points ?? [3, 2, 1]);
880
+ const scores = { ...mem.scores };
881
+ for (const [id, p] of Object.entries(points)) scores[id] = (scores[id] ?? 0) + p;
882
+ mem = { ...mem, scores, last: { ranking, points } };
883
+ const max = Math.max(1, rules.maxRounds ?? 50);
884
+ const done = mem.rounds > 0 && mem.round >= mem.rounds || mem.round >= max || guard("over", () => !!rules.over?.(api), false);
885
+ if (!done && rules.admit === "round") {
886
+ const ready = room.online.filter((p) => !p.idle && !room.isParticipant(p.id)).map((p) => p.id);
887
+ if (ready.length > 0) room.admit(ready);
888
+ }
889
+ write({ phase: "roundEnd", until: matchMs() + Math.max(0, (rules.roundEndSeconds ?? 3) * 1e3), turn: null, done });
890
+ }
891
+ function finish() {
892
+ if (!mem) return;
893
+ const seats = seatsOf(mem);
894
+ const final = rules.final ? guard("final", () => rules.final(api), void 0) : void 0;
895
+ const placings = final ? rankingOf(final, seats.map((s) => s.id)) : null;
896
+ const results = standingsOf(seats, mem.scores, placings);
897
+ const wait = Math.max(0, (rules.resultsSeconds ?? 10) * 1e3);
898
+ write({ phase: "results", until: wait > 0 ? matchMs() + wait : 0, results, turn: null });
899
+ }
900
+ const turnLimit = () => numberFrom(rules.turns?.seconds ?? 30, settings(), 30) * 1e3;
901
+ function firstTurn(now) {
902
+ const seats = seatsOf(mem).filter((s) => !s.left);
903
+ if (seats.length === 0) return null;
904
+ const order = rules.turns?.order === "random" ? createRandom(`${room.match.seed ?? 0}:ow.order:${mem.round}`).shuffle(seats.map((s) => s.id)) : seats.map((s) => s.id);
905
+ const limit = turnLimit();
906
+ return { n: 1, order, i: 0, id: order[0], until: limit > 0 ? now + limit : 0 };
907
+ }
908
+ function beginTurn() {
909
+ const seat = mem?.turn ? seatsOf(mem).find((s) => s.id === mem.turn.id) : void 0;
910
+ if (seat) guard("onTurn", () => rules.onTurn?.(api, seat), void 0);
911
+ }
912
+ function tickTurn(now) {
913
+ const turn = mem.turn;
914
+ const seat = seatsOf(mem).find((s) => s.id === turn.id);
915
+ let advance = turnEnd === turn.n || !seat || seat.left;
916
+ if (!advance && seat && !seat.bot) {
917
+ if (seat.away) {
918
+ if (!awaySince.has(seat.id)) awaySince.set(seat.id, now);
919
+ if (now - awaySince.get(seat.id) >= Math.max(0, rules.turns?.awaySeconds ?? 10) * 1e3) advance = true;
920
+ } else awaySince.delete(seat.id);
921
+ }
922
+ if (!advance && turn.until > 0 && now >= turn.until && seat) {
923
+ guard("turnTimeout", () => rules.turnTimeout?.(api, seat), void 0);
924
+ advance = true;
925
+ }
926
+ if (!advance || pendingEnd || !mem || mem.phase !== "round" || mem.turn?.n !== turn.n) return;
927
+ const perRound = Math.max(0, rules.turns?.perRound ?? 0);
928
+ if (perRound > 0 && turn.n >= perRound) return endRoundNow({ outcome: void 0, given: false });
929
+ const seats = seatsOf(mem);
930
+ let i = turn.i;
931
+ for (let tries = 0; tries < turn.order.length; tries++) {
932
+ i = (i + 1) % turn.order.length;
933
+ if (seats.some((s) => s.id === turn.order[i] && !s.left)) break;
934
+ }
935
+ const limit = turnLimit();
936
+ write({ turn: { n: turn.n + 1, order: turn.order, i, id: turn.order[i], until: limit > 0 ? now + limit : 0 } });
937
+ beginTurn();
938
+ }
939
+ const endTurnCall = host.rpc("ow.endTurn", {
940
+ args: { r: t.int(0, 1e5), n: t.int(0, 1e6) },
941
+ rate: 5,
942
+ validate: (from, args) => mem?.phase === "round" && mem.round === args.r && mem.turn?.id === from.id && mem.turn.n === args.n || "not your turn",
943
+ run: (_from, args) => {
944
+ turnEnd = args.n;
945
+ requestTick();
946
+ }
947
+ });
948
+ let syncQueued = false;
949
+ function requestSync() {
950
+ if (syncQueued || disposed) return;
951
+ syncQueued = true;
952
+ queueMicrotask(() => {
953
+ syncQueued = false;
954
+ sync();
955
+ });
956
+ }
957
+ function sync() {
958
+ if (disposed) return;
959
+ const rec = current();
960
+ const phase = phaseOf(rec);
961
+ if (phase !== shownPhase) {
962
+ const previous = shownPhase;
963
+ shownPhase = phase;
964
+ emit("phase", phase, previous);
965
+ }
966
+ if (rec && phase === "round") {
967
+ const key = `${rec.mid}.${rec.round}`;
968
+ if (key !== shownRound) {
969
+ shownRound = key;
970
+ emit("round", rec.round, { resumed: firstRecord && room.matchNow() - rec.started > 1500 });
971
+ }
972
+ const turn = api.turn;
973
+ const turnKey = turn ? `${key}.${turn.n}` : "";
974
+ if (turn && turnKey !== shownTurn) {
975
+ shownTurn = turnKey;
976
+ emit("turn", turn);
977
+ }
978
+ }
979
+ if (rec) firstRecord = false;
980
+ if (rec && phase === "results" && rec.results && room.match.phase === "playing") bank(rec);
981
+ emit("change");
982
+ }
983
+ function bank(rec) {
984
+ const results = rec.results;
985
+ const mine = results.find((s) => s.id === me()) ?? null;
986
+ if (room.ranked && mine && reported !== rec.mid) {
987
+ reported = rec.mid;
988
+ const groups = /* @__PURE__ */ new Map();
989
+ for (const s of results) if (!s.bot) (groups.get(s.place) ?? groups.set(s.place, []).get(s.place)).push(s.id);
990
+ const ranking = [...groups.entries()].sort((a, b) => a[0] - b[0]).map(([, group]) => group);
991
+ if (ranking.length >= 2) room.reportResult({ ranking });
992
+ else if (ranking.length === 1 && ranking[0].length >= 2) room.reportResult({ draw: true });
993
+ }
994
+ if (banked === rec.mid || room.private?.["ow.done"] === rec.mid) return;
995
+ banked = rec.mid;
996
+ room.setPrivate("ow.done", rec.mid);
997
+ emit("results", results, mine);
998
+ }
999
+ const view = () => current();
1000
+ const api = {
1001
+ get room() {
1002
+ return room;
1003
+ },
1004
+ get phase() {
1005
+ return phaseOf(view());
1006
+ },
1007
+ get paused() {
1008
+ return room.match.phase === "playing" && !room.running;
1009
+ },
1010
+ get isHost() {
1011
+ return acting();
1012
+ },
1013
+ get round() {
1014
+ return view()?.round ?? 0;
1015
+ },
1016
+ get rounds() {
1017
+ return view()?.rounds ?? (room.match.phase === "lobby" ? total() : 0);
1018
+ },
1019
+ get roundId() {
1020
+ const rec = view();
1021
+ return rec ? `${rec.mid}.${rec.round}` : "";
1022
+ },
1023
+ get seats() {
1024
+ return seatsOf(view());
1025
+ },
1026
+ get present() {
1027
+ return seatsOf(view()).filter((s) => s.bot || !s.away && !s.idle && !s.left);
1028
+ },
1029
+ get me() {
1030
+ return room.match.phase === "lobby" ? null : seatsOf(view()).find((s) => s.me) ?? null;
1031
+ },
1032
+ get spectating() {
1033
+ return room.spectating;
1034
+ },
1035
+ get scores() {
1036
+ return view()?.scores ?? {};
1037
+ },
1038
+ get standings() {
1039
+ const rec = view();
1040
+ if (!rec) return [];
1041
+ return rec.phase === "results" && rec.results ? rec.results : standingsOf(seatsOf(rec), rec.scores, null);
1042
+ },
1043
+ get results() {
1044
+ const rec = view();
1045
+ return rec?.phase === "results" ? rec.results : null;
1046
+ },
1047
+ get last() {
1048
+ const rec = view();
1049
+ return rec?.phase === "roundEnd" ? rec.last : null;
1050
+ },
1051
+ get turn() {
1052
+ const rec = view();
1053
+ if (!rec || rec.phase !== "round" || !rec.turn) return null;
1054
+ const seat = seatsOf(rec).find((s) => s.id === rec.turn.id) ?? null;
1055
+ return { n: rec.turn.n, id: rec.turn.id, seat, mine: rec.turn.id === me() };
1056
+ },
1057
+ get data() {
1058
+ return view()?.data ?? null;
1059
+ },
1060
+ get settings() {
1061
+ return settings();
1062
+ },
1063
+ get countdown() {
1064
+ const m = room.match;
1065
+ return m.phase === "starting" && m.startsAt !== void 0 ? Math.max(0, (m.startsAt - platformNow2()) / 1e3) : null;
1066
+ },
1067
+ get secondsLeft() {
1068
+ const rec = view();
1069
+ if (!rec || room.match.phase !== "playing") return null;
1070
+ const until = rec.phase === "round" && rec.turn && rec.turn.until > 0 ? rec.turn.until : rec.until;
1071
+ return until > 0 ? Math.max(0, (until - matchMs()) / 1e3) : null;
1072
+ },
1073
+ get elapsed() {
1074
+ const rec = view();
1075
+ return rec && rec.phase === "round" ? Math.max(0, matchMs() - rec.started) : 0;
1076
+ },
1077
+ get lobby() {
1078
+ const humans = room.online.length;
1079
+ return {
1080
+ players: [...room.players.values()].map((p) => ({ id: p.id, name: p.name, ready: !!p.ready || room.kind !== "public" && p.id === room.host, host: p.id === room.host, me: p.id === me(), away: p.connected === false })),
1081
+ canStart: room.canStart,
1082
+ waitingFor: room.notReady.map((p) => p.name),
1083
+ bots: Math.max(0, Math.min(30 - humans, botFill - humans))
1084
+ };
1085
+ },
1086
+ ready(on = true) {
1087
+ guard("match.ready", () => room.setReady(on !== false), void 0);
1088
+ },
1089
+ start() {
1090
+ guard("match.start", () => room.startMatch(), void 0);
1091
+ },
1092
+ endTurn() {
1093
+ const turn = view()?.turn;
1094
+ if (!turn || view()?.phase !== "round") return;
1095
+ if (acting()) {
1096
+ turnEnd = turn.n;
1097
+ requestTick();
1098
+ } else if (turn.id === me()) endTurnCall({ r: view()?.round ?? 0, n: turn.n });
1099
+ },
1100
+ random(name) {
1101
+ return createRandom(`${room.match.seed ?? 0}:${String(name)}`);
1102
+ },
1103
+ on(event, listener) {
1104
+ let set = listeners.get(event);
1105
+ if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
1106
+ set.add(listener);
1107
+ return () => void set.delete(listener);
1108
+ },
1109
+ setData(value) {
1110
+ if (!room.isHost || !mem || mem.by !== me() || mem.mid !== room.match.id || mem.phase === "results") {
1111
+ return warn("match-setData", "match.setData() did nothing: only the host writes the round data, during a match (check match.isHost)");
1112
+ }
1113
+ const next = typeof value === "function" ? guard("setData", () => {
1114
+ const draft = copy(mem.data);
1115
+ const out = value(draft);
1116
+ return out === void 0 ? draft : out;
1117
+ }, mem.data) : value;
1118
+ const clean = next === void 0 ? null : copy(next);
1119
+ if (clean === void 0) return warn("match-setData-plain", "match.setData(): the data must be plain data (numbers, text, lists, objects)");
1120
+ if (dataSchema && clean !== null) {
1121
+ const problem = check(dataSchema, clean, "match.data");
1122
+ if (problem) return warn("match-setData-schema", `match.setData() did nothing: ${problem}`);
1123
+ }
1124
+ write({ data: clean });
1125
+ },
1126
+ endRound(outcome) {
1127
+ if (!acting() || !mem || mem.phase !== "round") return;
1128
+ pendingEnd = { outcome, given: outcome !== void 0 };
1129
+ requestTick();
1130
+ },
1131
+ skip() {
1132
+ if (!acting() || !mem || mem.phase === "round") return;
1133
+ if (mem.phase === "results") return api.endMatch();
1134
+ write({ until: matchMs() });
1135
+ requestTick();
1136
+ },
1137
+ endMatch() {
1138
+ if (!room.isHost) return;
1139
+ if (mem) ended = mem.mid;
1140
+ guard("match.endMatch", () => room.endMatch(), void 0);
1141
+ },
1142
+ dispose() {
1143
+ if (disposed) return;
1144
+ disposed = true;
1145
+ clearInterval(timer);
1146
+ for (const off of offs) off();
1147
+ host.dispose();
1148
+ listeners.clear();
1149
+ },
1150
+ _useBots(fill, names, skill) {
1151
+ botFill = Math.max(0, Math.min(30, Math.floor(Number(fill) || 0)));
1152
+ if (Array.isArray(names) && names.length > 0) botNames = names.map((n) => String(n).slice(0, 24));
1153
+ if (typeof skill === "number" || Array.isArray(skill) && skill.length === 2) botSkill = skill;
1154
+ }
1155
+ };
1156
+ const timer = setInterval(tick, Math.max(16, rules.tick ?? 100));
1157
+ offs.push(room.on("state", (key) => key === MATCH_KEY && requestSync()));
1158
+ for (const event of ["match", "join", "leave", "away", "back", "idle", "ready", "settings", "reconnect", "disconnect"]) offs.push(room.on(event, () => requestSync()));
1159
+ offs.push(
1160
+ room.on("host", () => {
1161
+ adoptSince = 0;
1162
+ requestSync();
1163
+ requestTick();
1164
+ })
1165
+ );
1166
+ offs.push(room.on("match", () => requestTick()));
1167
+ offs.push(room.on("starting", () => void (rules.admit === "round" && room.isHost && room.kind !== "public" && room.setOpen(true))));
1168
+ offs.push(room.on("close", () => api.dispose()));
1169
+ requestSync();
1170
+ return api;
1171
+ }
1172
+ function defaultView(match, options = {}) {
1173
+ const doc = globalThis.document;
1174
+ if (!doc) return { remove() {
1175
+ } };
1176
+ const box = doc.createElement("div");
1177
+ box.setAttribute("data-onceworlds-match", "");
1178
+ box.style.cssText = "position:fixed;left:8px;top:8px;z-index:10;max-width:60vw;padding:6px 8px;font:13px/1.4 system-ui,sans-serif;color:#fff;background:rgba(0,0,0,.55);border-radius:6px;white-space:pre-line";
1179
+ (options.parent ?? doc.body).appendChild(box);
1180
+ const button = (label, onClick) => {
1181
+ const b = doc.createElement("button");
1182
+ b.textContent = label;
1183
+ b.style.cssText = "margin:4px 4px 0 0;font:inherit";
1184
+ b.addEventListener("click", onClick);
1185
+ box.appendChild(b);
1186
+ };
1187
+ const name = (id) => match.seats.find((s) => s.id === id)?.name ?? "Someone";
1188
+ const table = (rows) => rows.map((s) => `${s.place}. ${s.name} ${s.points}`).join("\n");
1189
+ const draw = () => {
1190
+ const left = match.secondsLeft;
1191
+ const clock = left === null ? "" : ` ${Math.ceil(left)}s`;
1192
+ let text = "";
1193
+ switch (match.phase) {
1194
+ case "lobby":
1195
+ text = match.results ? `Last match
1196
+ ${table(match.results)}
1197
+ ` : "";
1198
+ text += match.lobby.waitingFor.length ? `Waiting for ${match.lobby.waitingFor.join(", ")}` : "Lobby";
1199
+ break;
1200
+ case "countdown":
1201
+ text = options.countdown && match.countdown !== null ? String(Math.ceil(match.countdown)) : "Starting";
1202
+ break;
1203
+ case "intro":
1204
+ text = match.round ? `Round ${match.round}${match.rounds ? ` of ${match.rounds}` : ""}` : "Get ready";
1205
+ break;
1206
+ case "round": {
1207
+ const turn = match.turn;
1208
+ text = `Round ${match.round}${match.rounds ? `/${match.rounds}` : ""}${clock}`;
1209
+ if (turn) text += turn.mine ? "\nYour turn" : `
1210
+ ${turn.seat?.name ?? "Someone"}'s turn`;
1211
+ text += `
1212
+ ${table(match.standings)}`;
1213
+ break;
1214
+ }
1215
+ case "roundEnd": {
1216
+ const first = match.last?.ranking[0];
1217
+ text = first?.length ? `${first.map(name).join(" and ")} ${first.length > 1 ? "tie" : "wins"} the round` : "No winner";
1218
+ text += `
1219
+ ${table(match.standings)}`;
1220
+ break;
1221
+ }
1222
+ case "results":
1223
+ text = `Results${clock}
1224
+ ${table(match.standings)}`;
1225
+ break;
1226
+ }
1227
+ if (match.paused) text += "\nWaiting for players";
1228
+ if (match.spectating) text += "\nWatching: you play the next match";
1229
+ box.textContent = text;
1230
+ if (match.phase === "lobby" && options.lobby) {
1231
+ if (match.lobby.canStart) button("Start", () => match.start());
1232
+ else if (!match.room.isHost || match.room.kind === "public") {
1233
+ const ready = !!match.room.me.ready;
1234
+ button(ready ? "Not ready" : "Ready", () => match.ready(!ready));
1235
+ }
1236
+ }
1237
+ if (match.turn?.mine) button("End turn", () => match.endTurn());
1238
+ };
1239
+ const off = match.on("change", draw);
1240
+ const timer = setInterval(draw, 250);
1241
+ draw();
1242
+ return {
1243
+ remove() {
1244
+ off();
1245
+ clearInterval(timer);
1246
+ box.remove();
1247
+ }
1248
+ };
1249
+ }
1250
+ export {
1251
+ MATCH_KEY,
1252
+ check,
1253
+ createHost,
1254
+ createMatch,
1255
+ createRandom,
1256
+ defaultView,
1257
+ rankingOf,
1258
+ t
1259
+ };