@yyyu-star/arcade-kit 0.0.0-stage → 0.10.1

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +249 -0
  2. package/LICENSE +21 -0
  3. package/README.md +440 -2
  4. package/dist/auth.d.ts +66 -0
  5. package/dist/auth.d.ts.map +1 -0
  6. package/dist/auth.js +86 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/client.d.ts +194 -0
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +379 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/fun-art.d.ts +41 -0
  13. package/dist/fun-art.d.ts.map +1 -0
  14. package/dist/fun-art.js +141 -0
  15. package/dist/fun-art.js.map +1 -0
  16. package/dist/fun.d.ts +87 -0
  17. package/dist/fun.d.ts.map +1 -0
  18. package/dist/fun.js +512 -0
  19. package/dist/fun.js.map +1 -0
  20. package/dist/game.d.ts +160 -0
  21. package/dist/game.d.ts.map +1 -0
  22. package/dist/game.js +41 -0
  23. package/dist/game.js.map +1 -0
  24. package/dist/guest.d.ts +60 -0
  25. package/dist/guest.d.ts.map +1 -0
  26. package/dist/guest.js +87 -0
  27. package/dist/guest.js.map +1 -0
  28. package/dist/internal.d.ts +33 -0
  29. package/dist/internal.d.ts.map +1 -0
  30. package/dist/internal.js +53 -0
  31. package/dist/internal.js.map +1 -0
  32. package/dist/meter.d.ts +102 -0
  33. package/dist/meter.d.ts.map +1 -0
  34. package/dist/meter.js +207 -0
  35. package/dist/meter.js.map +1 -0
  36. package/dist/protocol.d.ts +227 -0
  37. package/dist/protocol.d.ts.map +1 -0
  38. package/dist/protocol.js +65 -0
  39. package/dist/protocol.js.map +1 -0
  40. package/dist/rng.d.ts +40 -0
  41. package/dist/rng.d.ts.map +1 -0
  42. package/dist/rng.js +119 -0
  43. package/dist/rng.js.map +1 -0
  44. package/dist/room.d.ts +352 -0
  45. package/dist/room.d.ts.map +1 -0
  46. package/dist/room.js +1563 -0
  47. package/dist/room.js.map +1 -0
  48. package/dist/testing.d.ts +125 -0
  49. package/dist/testing.d.ts.map +1 -0
  50. package/dist/testing.js +272 -0
  51. package/dist/testing.js.map +1 -0
  52. package/dist/worker.d.ts +89 -0
  53. package/dist/worker.d.ts.map +1 -0
  54. package/dist/worker.js +177 -0
  55. package/dist/worker.js.map +1 -0
  56. package/package.json +107 -4
  57. package/src/auth.ts +100 -0
  58. package/src/client.ts +438 -0
  59. package/src/fun-art.ts +184 -0
  60. package/src/fun.ts +626 -0
  61. package/src/game.ts +200 -0
  62. package/src/guest.ts +122 -0
  63. package/src/internal.ts +64 -0
  64. package/src/meter.ts +255 -0
  65. package/src/protocol.ts +243 -0
  66. package/src/rng.ts +131 -0
  67. package/src/room.ts +1677 -0
  68. package/src/testing.ts +300 -0
  69. package/src/worker.ts +229 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * 每个游戏 Worker 一个的"计量"(Durable Object,全局只有一个实例,名字固定):管住免费版每天 10 万行写入(docs/platform-backend.md 4.7)。
3
+ * 同一个对象里放三样东西,一次 RPC 办完,比"每人一个小 DO + 一个计量"少一次请求、少写建表那几行:
4
+ * - 当天(UTC)全站的写入行数:房间自己数写了几行,攒够一批(RoomLimits.meterBatch,默认 200)或者结束时报一次;
5
+ * - 当天全站开了几个房间(最后一道闸);
6
+ * - 每个人现在开着的房间、当天开了几个(按人的开房额度)。
7
+ *
8
+ * 门槛(QuotaLimits,游戏在 wrangler.jsonc 的 vars.KIT_QUOTA 里改,不用改代码):
9
+ * - 写入到 busyWrites(默认 6 万):不开新房、不进新人,在打的照常;
10
+ * - 写入到 stopWrites(默认 8.5 万):在打的房间在这一段结束(RoomGame.breakPoint)时体面收场;
11
+ * - 开房先过按人额度,再过全站额度。
12
+ *
13
+ * 写入:开房(占额度)写 2 行(这个人那一行、当天那一行),报一次写 1 行,结束(还额度加最后一报)写 2 行;计量自己写的也记进当天的数。
14
+ * 自愈:每个开着的房间记一个"最晚到什么时候"(开房时间 + 房间寿命 + 1 分钟)。房间关的时候来还额度,
15
+ * 还不上(计量连不上、房间被驱逐时出错)就等到点自动作废,不会把人永远卡在"你已经有一个房间了"。
16
+ * 当天的数在 UTC 0 点后的第一次调用时清零(和免费版额度重置的时间一样);房间跨天报上来的写入记在新的一天,偏保守。
17
+ *
18
+ * 这里的方法只给 kit 自己调(房间经 RPC),游戏不直接用;kitUsage 给运维和测试看。
19
+ */
20
+ import { DurableObject } from 'cloudflare:workers';
21
+ export interface QuotaLimits {
22
+ /** 每人同时开着的房间数(自己开的,打完、关掉、到寿命就还) */
23
+ roomsPerUser: number;
24
+ /** 每人每天(UTC)最多开几个房间 */
25
+ dailyRoomsPerUser: number;
26
+ /** 全站每天(UTC)最多开几个房间 */
27
+ dailyRooms: number;
28
+ /** 当天写入到这么多行:不开新房、不进新人 */
29
+ busyWrites: number;
30
+ /** 当天写入到这么多行:在打的房间这一段打完收场 */
31
+ stopWrites: number;
32
+ }
33
+ export declare const DEFAULT_QUOTA: QuotaLimits;
34
+ /** KIT_QUOTA 里由房间读的字段(0.7.0):emoteMs 每多少毫秒补一个短语令牌(0 不限速),emoteBurst 最多攒几个 */
35
+ export declare const ROOM_QUOTA_FIELDS: readonly string[];
36
+ /** 计量的绑定名和环境变量名固定(和 ROOM、ARCADE 一样)。 */
37
+ export interface MeterEnv {
38
+ METER?: DurableObjectNamespace;
39
+ /** 部分覆盖 DEFAULT_QUOTA,比如 { "dailyRooms": 500 };wrangler.jsonc 的 vars 里写对象,或者写成 JSON 字符串 */
40
+ KIT_QUOTA?: (Partial<QuotaLimits> & {
41
+ emoteMs?: number;
42
+ emoteBurst?: number;
43
+ }) | string;
44
+ }
45
+ /** ok 照常;busy 不开新房、不进新人;stop 在打的这一段打完收场。 */
46
+ export type MeterLevel = 'ok' | 'busy' | 'stop';
47
+ export type Reserve = {
48
+ ok: true;
49
+ } | {
50
+ ok: false;
51
+ error: 'has_room';
52
+ code: string;
53
+ } | {
54
+ ok: false;
55
+ error: 'daily_limit' | 'busy';
56
+ };
57
+ export interface Usage {
58
+ day: string;
59
+ writes: number;
60
+ rooms: number;
61
+ level: MeterLevel;
62
+ limits: QuotaLimits;
63
+ }
64
+ export declare class RoomMeter extends DurableObject<MeterEnv> {
65
+ private ready;
66
+ private limitsCache;
67
+ /** 开房前占一个名额(房间调)。房间没建成要调 kitRelease(…, cancel) 退回。 */
68
+ kitReserve(userId: string, code: string, until: number): Promise<Reserve>;
69
+ /**
70
+ * 房间结束(打完、关掉)时还名额,顺便报最后一批写入(writes 可以是 0)。cancel:房间没建成,开房次数也退回。
71
+ * 名额已经不在(还过一次、或者已经到点作废)就只记写入,可以重复调。
72
+ */
73
+ kitRelease(userId: string, code: string, writes: number, cancel?: boolean): Promise<MeterLevel>;
74
+ /** 报一批写入(房间攒够一批时调),回现在的门槛。 */
75
+ kitReport(writes: number): Promise<MeterLevel>;
76
+ /** 只读:现在的门槛(房间放新人进来之前问,不写库)。 */
77
+ kitLevel(): Promise<MeterLevel>;
78
+ /** 只读:当天的数,给运维和测试看。 */
79
+ kitUsage(): Promise<Usage>;
80
+ private ensure;
81
+ /** 当天那一行;过了 UTC 0 点就是新的一天,从 0 算(不先写,下次保存时一起写)。 */
82
+ private day;
83
+ private saveDay;
84
+ /** 这个人那一行:换了一天开房次数清零,到点的房间作废(只在内存里,下次保存时才写)。 */
85
+ private user;
86
+ /** 存这个人那一行,返回写了几行。没有开着的房间、当天也没开过的,删掉这一行,不留着占地方。 */
87
+ private saveUser;
88
+ /** 门槛按调用时的环境变量算;配错的字段记日志、用默认值(不让一个手误把额度放开或关死)。 */
89
+ private limits;
90
+ }
91
+ /**
92
+ * 房间以外的 DO(比如游戏自己的钱包)把写了几行记进当天的数,回现在的门槛(0.3.0)。
93
+ * 计量每收一次报数自己写 1 行,所以只适合写得少的地方(钱包一次操作报一次,就是每次多 1 行);房间自己会攒一批再报,不用它。
94
+ * 没绑 METER 时什么都不做、回 ok(和房间一样,只为平滑升级)。连不上时抛错,调用方决定怎么办
95
+ */
96
+ export declare function meterReport(env: MeterEnv, writes: number): Promise<MeterLevel>;
97
+ /** 只读:现在的门槛(0.3.0;写之前问一下,stop 时就别写了)。没绑 METER 回 ok,连不上抛错。 */
98
+ export declare function meterLevel(env: MeterEnv): Promise<MeterLevel>;
99
+ /** 解析 KIT_QUOTA:对象或 JSON 字符串,只认 DEFAULT_QUOTA 里有的字段、非负整数;别的记日志后忽略。 */
100
+ export declare function parseQuota(raw: unknown): QuotaLimits;
101
+ export declare function levelOf(writes: number, lim: QuotaLimits): MeterLevel;
102
+ //# sourceMappingURL=meter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.d.ts","sourceRoot":"","sources":["../src/meter.ts"],"names":[],"mappings":"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"}
package/dist/meter.js ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * 每个游戏 Worker 一个的"计量"(Durable Object,全局只有一个实例,名字固定):管住免费版每天 10 万行写入(docs/platform-backend.md 4.7)。
3
+ * 同一个对象里放三样东西,一次 RPC 办完,比"每人一个小 DO + 一个计量"少一次请求、少写建表那几行:
4
+ * - 当天(UTC)全站的写入行数:房间自己数写了几行,攒够一批(RoomLimits.meterBatch,默认 200)或者结束时报一次;
5
+ * - 当天全站开了几个房间(最后一道闸);
6
+ * - 每个人现在开着的房间、当天开了几个(按人的开房额度)。
7
+ *
8
+ * 门槛(QuotaLimits,游戏在 wrangler.jsonc 的 vars.KIT_QUOTA 里改,不用改代码):
9
+ * - 写入到 busyWrites(默认 6 万):不开新房、不进新人,在打的照常;
10
+ * - 写入到 stopWrites(默认 8.5 万):在打的房间在这一段结束(RoomGame.breakPoint)时体面收场;
11
+ * - 开房先过按人额度,再过全站额度。
12
+ *
13
+ * 写入:开房(占额度)写 2 行(这个人那一行、当天那一行),报一次写 1 行,结束(还额度加最后一报)写 2 行;计量自己写的也记进当天的数。
14
+ * 自愈:每个开着的房间记一个"最晚到什么时候"(开房时间 + 房间寿命 + 1 分钟)。房间关的时候来还额度,
15
+ * 还不上(计量连不上、房间被驱逐时出错)就等到点自动作废,不会把人永远卡在"你已经有一个房间了"。
16
+ * 当天的数在 UTC 0 点后的第一次调用时清零(和免费版额度重置的时间一样);房间跨天报上来的写入记在新的一天,偏保守。
17
+ *
18
+ * 这里的方法只给 kit 自己调(房间经 RPC),游戏不直接用;kitUsage 给运维和测试看。
19
+ */
20
+ import { DurableObject } from 'cloudflare:workers';
21
+ import { meterStub } from './internal.js';
22
+ export const DEFAULT_QUOTA = {
23
+ roomsPerUser: 1,
24
+ dailyRoomsPerUser: 6,
25
+ dailyRooms: 300,
26
+ busyWrites: 60_000,
27
+ stopWrites: 85_000,
28
+ };
29
+ /** KIT_QUOTA 里由房间读的字段(0.7.0):emoteMs 每多少毫秒补一个短语令牌(0 不限速),emoteBurst 最多攒几个 */
30
+ export const ROOM_QUOTA_FIELDS = ['emoteMs', 'emoteBurst'];
31
+ /** 房间寿命之外多留一分钟:房间到寿命那一刻的闹钟、关房、还额度都在这之前做完。 */
32
+ const EXPIRY_SLACK_MS = 60_000;
33
+ export class RoomMeter extends DurableObject {
34
+ ready = false;
35
+ limitsCache = null;
36
+ /** 开房前占一个名额(房间调)。房间没建成要调 kitRelease(…, cancel) 退回。 */
37
+ async kitReserve(userId, code, until) {
38
+ const now = Date.now();
39
+ const lim = this.limits();
40
+ const day = this.day(now);
41
+ const user = this.user(userId, day.day, now);
42
+ // 先说"回你的房间":在打的房间不受全站门槛影响,回去照样能玩
43
+ if (user.live.length >= lim.roomsPerUser)
44
+ return { ok: false, error: 'has_room', code: user.live[user.live.length - 1].code };
45
+ if (user.count >= lim.dailyRoomsPerUser)
46
+ return { ok: false, error: 'daily_limit' };
47
+ if (levelOf(day.writes, lim) !== 'ok' || day.rooms >= lim.dailyRooms)
48
+ return { ok: false, error: 'busy' };
49
+ user.count++;
50
+ user.live.push({ code, until: until + EXPIRY_SLACK_MS });
51
+ const w = this.saveUser(userId, user);
52
+ this.saveDay(day.day, day.writes + w + 1, day.rooms + 1);
53
+ return { ok: true };
54
+ }
55
+ /**
56
+ * 房间结束(打完、关掉)时还名额,顺便报最后一批写入(writes 可以是 0)。cancel:房间没建成,开房次数也退回。
57
+ * 名额已经不在(还过一次、或者已经到点作废)就只记写入,可以重复调。
58
+ */
59
+ async kitRelease(userId, code, writes, cancel = false) {
60
+ const now = Date.now();
61
+ const day = this.day(now);
62
+ const user = this.user(userId, day.day, now);
63
+ const i = user.live.findIndex((r) => r.code === code);
64
+ let w = 0;
65
+ if (i >= 0) {
66
+ user.live.splice(i, 1);
67
+ if (cancel && user.count > 0)
68
+ user.count--;
69
+ w = this.saveUser(userId, user);
70
+ }
71
+ const rooms = cancel && i >= 0 ? Math.max(0, day.rooms - 1) : day.rooms;
72
+ const total = day.writes + clean(writes) + w + 1;
73
+ this.saveDay(day.day, total, rooms);
74
+ return levelOf(total, this.limits());
75
+ }
76
+ /** 报一批写入(房间攒够一批时调),回现在的门槛。 */
77
+ async kitReport(writes) {
78
+ const day = this.day(Date.now());
79
+ const total = day.writes + clean(writes) + 1;
80
+ this.saveDay(day.day, total, day.rooms);
81
+ return levelOf(total, this.limits());
82
+ }
83
+ /** 只读:现在的门槛(房间放新人进来之前问,不写库)。 */
84
+ async kitLevel() {
85
+ return levelOf(this.day(Date.now()).writes, this.limits());
86
+ }
87
+ /** 只读:当天的数,给运维和测试看。 */
88
+ async kitUsage() {
89
+ const day = this.day(Date.now());
90
+ const limits = this.limits();
91
+ return { day: day.day, writes: day.writes, rooms: day.rooms, level: levelOf(day.writes, limits), limits };
92
+ }
93
+ // ---------- 存储 ----------
94
+ ensure() {
95
+ if (this.ready)
96
+ return;
97
+ const sql = this.ctx.storage.sql;
98
+ // 建表只有第一次真的写(之后 IF NOT EXISTS 什么都不做);建表那几行记不进当天的数,一个游戏一生只有一次
99
+ sql.exec('CREATE TABLE IF NOT EXISTS kit_day (k INTEGER PRIMARY KEY, day TEXT NOT NULL, writes INTEGER NOT NULL, rooms INTEGER NOT NULL)');
100
+ // WITHOUT ROWID:按 userId 存一棵树,改一个人只写 1 行(普通表还要多写一份索引)
101
+ sql.exec('CREATE TABLE IF NOT EXISTS kit_user (id TEXT PRIMARY KEY, v TEXT NOT NULL) WITHOUT ROWID');
102
+ this.ready = true;
103
+ }
104
+ /** 当天那一行;过了 UTC 0 点就是新的一天,从 0 算(不先写,下次保存时一起写)。 */
105
+ day(now) {
106
+ this.ensure();
107
+ const today = utcDay(now);
108
+ const row = this.ctx.storage.sql.exec('SELECT day, writes, rooms FROM kit_day WHERE k = 1').toArray()[0];
109
+ return row && row.day === today ? row : { day: today, writes: 0, rooms: 0 };
110
+ }
111
+ saveDay(day, writes, rooms) {
112
+ this.ctx.storage.sql.exec('INSERT INTO kit_day (k, day, writes, rooms) VALUES (1, ?, ?, ?) ON CONFLICT (k) DO UPDATE SET day = excluded.day, writes = excluded.writes, rooms = excluded.rooms', day, writes, rooms);
113
+ }
114
+ /** 这个人那一行:换了一天开房次数清零,到点的房间作废(只在内存里,下次保存时才写)。 */
115
+ user(userId, today, now) {
116
+ const raw = this.ctx.storage.sql.exec('SELECT v FROM kit_user WHERE id = ?', userId).toArray()[0];
117
+ let row = { day: today, count: 0, live: [] };
118
+ if (raw) {
119
+ try {
120
+ row = JSON.parse(raw.v);
121
+ }
122
+ catch {
123
+ console.error('meter user row unreadable', userId);
124
+ }
125
+ }
126
+ return { day: today, count: row.day === today ? row.count : 0, live: row.live.filter((r) => r.until > now) };
127
+ }
128
+ /** 存这个人那一行,返回写了几行。没有开着的房间、当天也没开过的,删掉这一行,不留着占地方。 */
129
+ saveUser(userId, row) {
130
+ const sql = this.ctx.storage.sql;
131
+ if (row.live.length === 0 && row.count === 0)
132
+ return sql.exec('DELETE FROM kit_user WHERE id = ?', userId).rowsWritten;
133
+ return sql.exec('INSERT INTO kit_user (id, v) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET v = excluded.v', userId, JSON.stringify(row)).rowsWritten;
134
+ }
135
+ /** 门槛按调用时的环境变量算;配错的字段记日志、用默认值(不让一个手误把额度放开或关死)。 */
136
+ limits() {
137
+ if (this.limitsCache)
138
+ return this.limitsCache;
139
+ this.limitsCache = parseQuota(this.env.KIT_QUOTA);
140
+ return this.limitsCache;
141
+ }
142
+ }
143
+ /**
144
+ * 房间以外的 DO(比如游戏自己的钱包)把写了几行记进当天的数,回现在的门槛(0.3.0)。
145
+ * 计量每收一次报数自己写 1 行,所以只适合写得少的地方(钱包一次操作报一次,就是每次多 1 行);房间自己会攒一批再报,不用它。
146
+ * 没绑 METER 时什么都不做、回 ok(和房间一样,只为平滑升级)。连不上时抛错,调用方决定怎么办
147
+ */
148
+ export async function meterReport(env, writes) {
149
+ if (!env.METER)
150
+ return 'ok';
151
+ return meterStub(env.METER).kitReport(writes);
152
+ }
153
+ /** 只读:现在的门槛(0.3.0;写之前问一下,stop 时就别写了)。没绑 METER 回 ok,连不上抛错。 */
154
+ export async function meterLevel(env) {
155
+ if (!env.METER)
156
+ return 'ok';
157
+ return meterStub(env.METER).kitLevel();
158
+ }
159
+ /** 解析 KIT_QUOTA:对象或 JSON 字符串,只认 DEFAULT_QUOTA 里有的字段、非负整数;别的记日志后忽略。 */
160
+ export function parseQuota(raw) {
161
+ let v = raw;
162
+ if (typeof raw === 'string') {
163
+ try {
164
+ v = JSON.parse(raw);
165
+ }
166
+ catch {
167
+ console.error('KIT_QUOTA is not JSON, using defaults');
168
+ v = null;
169
+ }
170
+ }
171
+ const out = { ...DEFAULT_QUOTA };
172
+ if (v === undefined || v === null)
173
+ return out;
174
+ if (typeof v !== 'object' || Array.isArray(v)) {
175
+ console.error('KIT_QUOTA is not an object, using defaults');
176
+ return out;
177
+ }
178
+ for (const [k, n] of Object.entries(v)) {
179
+ // 房间自己读的字段(0.7.0:快捷短语的节奏),计量不管
180
+ if (ROOM_QUOTA_FIELDS.includes(k))
181
+ continue;
182
+ if (!(k in DEFAULT_QUOTA))
183
+ console.error('KIT_QUOTA unknown field', k);
184
+ else if (typeof n !== 'number' || !Number.isInteger(n) || n < 0)
185
+ console.error('KIT_QUOTA bad value for', k);
186
+ else
187
+ out[k] = n;
188
+ }
189
+ if (out.stopWrites < out.busyWrites)
190
+ console.error('KIT_QUOTA stopWrites is below busyWrites');
191
+ return out;
192
+ }
193
+ export function levelOf(writes, lim) {
194
+ if (writes >= lim.stopWrites)
195
+ return 'stop';
196
+ if (writes >= lim.busyWrites)
197
+ return 'busy';
198
+ return 'ok';
199
+ }
200
+ function utcDay(now) {
201
+ return new Date(now).toISOString().slice(0, 10);
202
+ }
203
+ /** 房间报上来的行数:只认非负整数(RPC 那头是自己人,这里防的是自己算错) */
204
+ function clean(n) {
205
+ return Number.isInteger(n) && n > 0 ? n : 0;
206
+ }
207
+ //# sourceMappingURL=meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meter.js","sourceRoot":"","sources":["../src/meter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,227 @@
1
+ /**
2
+ * 房间和浏览器之间的消息(docs/platform-backend.md 4.3)。
3
+ *
4
+ * - 服务端说了算:客户端只发"我想做什么"(act 里的 action),合不合法、结果是什么都由房间算;
5
+ * - 每条下行消息带 seq(房间这一局的步数),客户端按 seq 去重、丢掉旧的;
6
+ * - 客户端的每个操作带 moveId,断线重连后重发没确认的操作,房间按 moveId 认出重复的;
7
+ * - 协议带版本号 v,新旧代码会并存(部署后正在打的房间还跑旧代码,4.5),所以消息格式只加字段、不改含义;
8
+ * - 心跳是纯文本 "ping" / "pong",由运行时自动应答(setWebSocketAutoResponse),不唤醒房间、不计费。
9
+ */
10
+ /** 协议版本:只在不兼容时加一。 */
11
+ export declare const PROTOCOL_VERSION = 1;
12
+ export declare const PING = "ping";
13
+ export declare const PONG = "pong";
14
+ /** 房间码:6 位数字(9.1)。 */
15
+ export declare const ROOM_CODE_RE: RegExp;
16
+ /**
17
+ * 关闭码。1008(违规)和 4000 段客户端都不重连(4.5);1006 等异常断开才重连。
18
+ * 1009 是消息太大(4.6),属于程序错误,也不重连。
19
+ */
20
+ export declare const CLOSE: {
21
+ /** 发得太快(令牌桶)或其他违规 */
22
+ readonly policy: 1008;
23
+ /** 入站消息超过大小上限 */
24
+ readonly tooBig: 1009;
25
+ /** 平台暂时认不了人(不可用、超时):稍后再试,客户端照常按退避重连 */
26
+ readonly tryLater: 1013;
27
+ /** 房间关了(打完、超时、没人、超过寿命或消息总数) */
28
+ readonly roomClosed: 4000;
29
+ /** 同一个账号在别处连上了,这条连接让位 */
30
+ readonly replaced: 4001;
31
+ /** 没入座的连接满了(RoomLimits.extraConnections,默认 4 条;入座的人不占这个数) */
32
+ readonly full: 4003;
33
+ /** 没有这个房间(码不对或已经关了) */
34
+ readonly noRoom: 4004;
35
+ /** 客户端太旧,刷新页面 */
36
+ readonly tooOld: 4005;
37
+ /** 这个人在这个房间的重连次数用完了(反复连上再断开) */
38
+ readonly tooManyJoins: 4029;
39
+ /** 这个人在这个房间发的消息太多(超过房间一生入站额度里他那一份):只断开他,房间照常 */
40
+ readonly userQuota: 4030;
41
+ /** 今天联机的额度快用完了,不进新人(0.2.0):已经在这个房间里坐过的人照常能回来 */
42
+ readonly busy: 4503;
43
+ /** 没登录,或者平台暂时认不出人:先登录再来 */
44
+ readonly unauthorized: 4401;
45
+ /** 不是本站页面发起的 */
46
+ readonly forbidden: 4403;
47
+ };
48
+ /**
49
+ * 4000(roomClosed)带的原因(0.2.0),客户端在 onStatus 的第三个参数里拿到。
50
+ * quota:今天全站的写入额度快用完了,房间在这一段(比如这一副)打完时收场。其他原因(打完、超时、没人)不带这个。
51
+ */
52
+ export declare const CLOSE_REASON: {
53
+ readonly quota: 'quota';
54
+ };
55
+ /**
56
+ * 开房(acceptCreate)失败时回的 JSON `{ error, code? }`(0.2.0):
57
+ * - login 401 没登录;forbidden 403 不是本站页面;
58
+ * - has_room 409 这个人已经有一个开着的房间(code 是那个房间的码,页面给"回到房间");
59
+ * - daily_limit 429 这个人今天开房次数用完了;
60
+ * - busy 503 今天联机的人太多了(全站写入或开房数到了门槛),明天再来;
61
+ * - unavailable 503 平台或计量暂时连不上、房间出错,稍后再试;
62
+ * - bad_options 400 开房时带的选项不合格(0.4.0,游戏 Worker 传了 options 时才会有)。
63
+ */
64
+ export type CreateError = 'login' | 'forbidden' | 'has_room' | 'daily_limit' | 'busy' | 'unavailable' | 'bad_options';
65
+ /** 不重连的关闭码。 */
66
+ export declare function isFinalClose(code: number): boolean;
67
+ /** 坐到某个座位(等人时选空位,开打后可以接管电脑的座位)。 */
68
+ export interface SitMsg {
69
+ t: 'sit';
70
+ seat: number;
71
+ }
72
+ /** 离开座位(等人时空出来;开打后由电脑接手)。 */
73
+ export interface LeaveMsg {
74
+ t: 'leave';
75
+ }
76
+ /** 开局:空座位由电脑补上。opts 是房主选的开局选项(0.3.0,游戏的 RoomGame.options 校验;不合格回 bad_options)。 */
77
+ export interface StartMsg {
78
+ t: 'start';
79
+ opts?: unknown;
80
+ }
81
+ /**
82
+ * 一个操作意图,内容由游戏定。moveId 由客户端生成(同一个操作重发时不变)。
83
+ * baseSeq 是做这个决定时看到的最新 seq:牌局在那之后变过(别人出了牌、超时代出了),房间回 stale 不做,
84
+ * 免得断线前点的旧操作在重连后被套到新的一手上。
85
+ */
86
+ export interface ActMsg<A = unknown> {
87
+ t: 'act';
88
+ moveId: string;
89
+ baseSeq: number;
90
+ action: A;
91
+ }
92
+ /**
93
+ * 快捷短语(0.6.0):只发游戏短语表里的编号(0 到 RoomGame.emotes - 1),不收自由文本,不用审内容。
94
+ * 只有入座的人能发(电脑座位由房间子类经 GameRoom.botEmote 发,0.10.0),每个座位一个令牌桶;房间只转发、不存,被拒回 error(见 EMOTE_ERRORS),页面可以不提示。
95
+ */
96
+ export interface EmoteMsg {
97
+ t: 'emote';
98
+ id: number;
99
+ /** 冲着哪个座位(0.7.0,可选):扔道具、点名嘲讽。得是有人的别的座位,不然回 bad_target;不带就和 0.6.0 一样 */
100
+ to?: number;
101
+ }
102
+ export type UpMsg<A = unknown> = SitMsg | LeaveMsg | StartMsg | ActMsg<A> | EmoteMsg;
103
+ export type SeatKind = 'empty' | 'human' | 'ai';
104
+ /** 座位的公开信息:谁坐着、是真人还是电脑、在不在线、是不是电脑在代打。 */
105
+ export interface SeatInfo {
106
+ kind: SeatKind;
107
+ /**
108
+ * 真人的昵称;开打后绑了人的座位由电脑代打时(主人离座了)是原主人的昵称,牌桌可以显示"电脑代某某打"(0.1.1)。
109
+ * 没绑人的电脑座位和空位为 null(电脑的名字由游戏按座位号自己定)
110
+ */
111
+ name: string | null;
112
+ /** 真人掉线了(座位还留着) */
113
+ away: boolean;
114
+ /** 真人掉线超过宽限,电脑在代打;回来就交还 */
115
+ auto: boolean;
116
+ /**
117
+ * 开打后这个座位已经绑了人(主人离座了也算):kind 是 'ai' 时别人接管不了(seat_reserved),大厅不用给它挂"接管"按钮;
118
+ * 只有原主人能回来(看 you.bound)。等人阶段都是 false。0.1.1 起房间总会带,旧房间代码不带
119
+ */
120
+ reserved?: boolean;
121
+ }
122
+ /**
123
+ * 这条连接是谁:seat 是现在坐的座位(没入座为 null);
124
+ * bound 是这个人开打后绑定的座位(离座了也还是它,可以"回到我的座位";还没开打、一局里没坐过为 null)。
125
+ * bound 是 0.1.1 加的,房间总会带
126
+ */
127
+ export interface You {
128
+ seat: number | null;
129
+ bound?: number | null;
130
+ }
131
+ export type RoomStatus = 'waiting' | 'playing' | 'over';
132
+ export interface RoomInfo {
133
+ code: string;
134
+ status: RoomStatus;
135
+ seats: SeatInfo[];
136
+ /**
137
+ * 当前要出手的座位和截止时间(毫秒时间戳),没在等人出手时为 null。
138
+ * seats(0.1.1):游戏写了 RoomGame.actors 才有,是这一刻还能出手的全部座位(一组人时就是还没点的那几个),seat 是其中第一个;
139
+ * 截止时间对整组只有一个
140
+ */
141
+ turn: {
142
+ seat: number;
143
+ deadline: number | null;
144
+ seats?: number[];
145
+ } | null;
146
+ /**
147
+ * 开房时定下的开局选项(0.4.0):RoomGame.options 校验过的原样,进房的人开打前就知道这一局怎么打(档位、几个座位)。
148
+ * 开房时没带选项的房间没有这个字段(开打时由 start 定,和以前一样)
149
+ */
150
+ opts?: unknown;
151
+ /**
152
+ * 正在"停一会儿再接着来"(0.5.0,RoomGame.pause):until 是房间接着推进的时间戳(毫秒),页面拿它画倒计时。
153
+ * 只有在停时才有,这时 turn 是 null;游戏的 resume 出错时房间会把它往后推
154
+ */
155
+ pause?: {
156
+ until: number;
157
+ };
158
+ }
159
+ /** 一帧画面:按座位过滤过的游戏视图,delay 是相对上一帧的播放延迟(电脑连续出牌时用,4.4)。 */
160
+ export interface Frame<V = unknown> {
161
+ view: V;
162
+ delay: number;
163
+ }
164
+ /** 进房(含重连)时的全量快照。 */
165
+ export interface WelcomeMsg<V = unknown> {
166
+ t: 'welcome';
167
+ v: number;
168
+ minClientVersion: number;
169
+ seq: number;
170
+ you: You;
171
+ room: RoomInfo;
172
+ view: V | null;
173
+ /**
174
+ * 快捷短语的节奏(0.7.0,游戏有短语时才带):每 ms 毫秒补 1 个令牌、最多攒 burst 个。RoomClient 按它在本地排队发,
175
+ * 用户感觉不到限速;运维在 KIT_QUOTA 里改,不用发版
176
+ */
177
+ emote?: {
178
+ ms: number;
179
+ burst: number;
180
+ };
181
+ }
182
+ /** 每一步之后的状态。ack 只发给做这一步的那条连接:对应它的 moveId。 */
183
+ export interface StateMsg<V = unknown> {
184
+ t: 'state';
185
+ seq: number;
186
+ you: You;
187
+ room: RoomInfo;
188
+ /** 至少一帧;最后一帧就是当前状态 */
189
+ frames: Frame<V>[];
190
+ ack?: string;
191
+ /**
192
+ * 别人看不出来的一步(0.7.1):不轮流的操作没改任何别人能看到的东西(比如声明窗口里真人答"碰"),房间只回给出手的人、seq 不加(和上一条一样)。
193
+ * 只和 ack 一起出现;RoomClient 0.7.1 起认它(seq 一样也交给页面),旧的 RoomClient 当重复丢掉
194
+ */
195
+ quiet?: true;
196
+ }
197
+ /**
198
+ * 操作被拒(不合法、不该你、格式不对、牌局已经变了 stale、不是房主 not_host)。不改 seq。
199
+ * code 是固定的错误码,游戏的 apply 返回的错误码也原样放在这里(见 RoomGame.apply)。
200
+ */
201
+ export interface ErrorMsg {
202
+ t: 'error';
203
+ code: string;
204
+ moveId?: string;
205
+ }
206
+ /**
207
+ * 有人发了快捷短语(0.6.0):seat 是房间的座位号(游戏按自己的视角换算),id 是短语编号。
208
+ * 发给房间里所有收到过快照的连接(含发的人自己,页面收到再冒气泡)。不带 seq,不改牌局,断线时错过的不补发。
209
+ * 不在 DownMsg 里:RoomClient 把它交给 onEmote,不进 onMessage,不用短语的游戏按 DownMsg 写的代码一行不用改
210
+ */
211
+ export interface EmotedMsg {
212
+ t: 'emote';
213
+ seat: number;
214
+ id: number;
215
+ /** 发的时候指定了对象才有(0.7.0),房间的座位号 */
216
+ to?: number;
217
+ /** 电脑座位发的(0.10.0,GameRoom.botEmote)才有;页面可以不区分 */
218
+ bot?: true;
219
+ }
220
+ /**
221
+ * 快捷短语被拒时 error 的 code(0.6.0):no_emotes 这个游戏没有短语表;bad_emote 编号不对;
222
+ * not_seated 没入座(旁观的不能发);room_over 房间打完了;emote_too_fast 这个座位离上一句不到 emoteMs;
223
+ * bad_target(0.7.0)带的 to 不是有人的别的座位
224
+ */
225
+ export declare const EMOTE_ERRORS: readonly ['no_emotes', 'bad_emote', 'not_seated', 'room_over', 'emote_too_fast', 'bad_target'];
226
+ export type DownMsg<V = unknown> = WelcomeMsg<V> | StateMsg<V> | ErrorMsg;
227
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 房间和浏览器之间的消息(docs/platform-backend.md 4.3)。
3
+ *
4
+ * - 服务端说了算:客户端只发"我想做什么"(act 里的 action),合不合法、结果是什么都由房间算;
5
+ * - 每条下行消息带 seq(房间这一局的步数),客户端按 seq 去重、丢掉旧的;
6
+ * - 客户端的每个操作带 moveId,断线重连后重发没确认的操作,房间按 moveId 认出重复的;
7
+ * - 协议带版本号 v,新旧代码会并存(部署后正在打的房间还跑旧代码,4.5),所以消息格式只加字段、不改含义;
8
+ * - 心跳是纯文本 "ping" / "pong",由运行时自动应答(setWebSocketAutoResponse),不唤醒房间、不计费。
9
+ */
10
+ /** 协议版本:只在不兼容时加一。 */
11
+ export const PROTOCOL_VERSION = 1;
12
+ export const PING = 'ping';
13
+ export const PONG = 'pong';
14
+ /** 房间码:6 位数字(9.1)。 */
15
+ export const ROOM_CODE_RE = /^[0-9]{6}$/;
16
+ /**
17
+ * 关闭码。1008(违规)和 4000 段客户端都不重连(4.5);1006 等异常断开才重连。
18
+ * 1009 是消息太大(4.6),属于程序错误,也不重连。
19
+ */
20
+ export const CLOSE = {
21
+ /** 发得太快(令牌桶)或其他违规 */
22
+ policy: 1008,
23
+ /** 入站消息超过大小上限 */
24
+ tooBig: 1009,
25
+ /** 平台暂时认不了人(不可用、超时):稍后再试,客户端照常按退避重连 */
26
+ tryLater: 1013,
27
+ /** 房间关了(打完、超时、没人、超过寿命或消息总数) */
28
+ roomClosed: 4000,
29
+ /** 同一个账号在别处连上了,这条连接让位 */
30
+ replaced: 4001,
31
+ /** 没入座的连接满了(RoomLimits.extraConnections,默认 4 条;入座的人不占这个数) */
32
+ full: 4003,
33
+ /** 没有这个房间(码不对或已经关了) */
34
+ noRoom: 4004,
35
+ /** 客户端太旧,刷新页面 */
36
+ tooOld: 4005,
37
+ /** 这个人在这个房间的重连次数用完了(反复连上再断开) */
38
+ tooManyJoins: 4029,
39
+ /** 这个人在这个房间发的消息太多(超过房间一生入站额度里他那一份):只断开他,房间照常 */
40
+ userQuota: 4030,
41
+ /** 今天联机的额度快用完了,不进新人(0.2.0):已经在这个房间里坐过的人照常能回来 */
42
+ busy: 4503,
43
+ /** 没登录,或者平台暂时认不出人:先登录再来 */
44
+ unauthorized: 4401,
45
+ /** 不是本站页面发起的 */
46
+ forbidden: 4403,
47
+ };
48
+ /**
49
+ * 4000(roomClosed)带的原因(0.2.0),客户端在 onStatus 的第三个参数里拿到。
50
+ * quota:今天全站的写入额度快用完了,房间在这一段(比如这一副)打完时收场。其他原因(打完、超时、没人)不带这个。
51
+ */
52
+ export const CLOSE_REASON = {
53
+ quota: 'quota',
54
+ };
55
+ /** 不重连的关闭码。 */
56
+ export function isFinalClose(code) {
57
+ return code === CLOSE.policy || code === CLOSE.tooBig || (code >= 4000 && code < 5000);
58
+ }
59
+ /**
60
+ * 快捷短语被拒时 error 的 code(0.6.0):no_emotes 这个游戏没有短语表;bad_emote 编号不对;
61
+ * not_seated 没入座(旁观的不能发);room_over 房间打完了;emote_too_fast 这个座位离上一句不到 emoteMs;
62
+ * bad_target(0.7.0)带的 to 不是有人的别的座位
63
+ */
64
+ export const EMOTE_ERRORS = ['no_emotes', 'bad_emote', 'not_seated', 'room_over', 'emote_too_fast', 'bad_target'];
65
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,qBAAqB;AACrB,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC;AAC3B,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC;AAE3B,sBAAsB;AACtB,MAAM,CAAC,MAAM,YAAY,GAAG,YAAY,CAAC;AAEzC;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,qBAAqB;IACrB,MAAM,EAAE,IAAI;IACZ,iBAAiB;IACjB,MAAM,EAAE,IAAI;IACZ,uCAAuC;IACvC,QAAQ,EAAE,IAAI;IACd,+BAA+B;IAC/B,UAAU,EAAE,IAAI;IAChB,yBAAyB;IACzB,QAAQ,EAAE,IAAI;IACd,6DAA6D;IAC7D,IAAI,EAAE,IAAI;IACV,uBAAuB;IACvB,MAAM,EAAE,IAAI;IACZ,iBAAiB;IACjB,MAAM,EAAE,IAAI;IACZ,gCAAgC;IAChC,YAAY,EAAE,IAAI;IAClB,gDAAgD;IAChD,SAAS,EAAE,IAAI;IACf,gDAAgD;IAChD,IAAI,EAAE,IAAI;IACV,2BAA2B;IAC3B,YAAY,EAAE,IAAI;IAClB,gBAAgB;IAChB,SAAS,EAAE,IAAI;CACP,CAAC;AAEX;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,KAAK,EAAE,OAAO;CACN,CAAC;AAaX,eAAe;AACf,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,IAAI,KAAK,KAAK,CAAC,MAAM,IAAI,IAAI,KAAK,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC;AACzF,CAAC;AAiKD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,YAAY,CAAU,CAAC"}