@zhuweiyou/dsh-holdem 0.1.2 → 0.1.4

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/lib/index.js CHANGED
@@ -15,7 +15,10 @@ const PACKAGE_NAME = "@zhuweiyou/dsh-holdem";
15
15
  const SUBPROTOCOL = "holdem.v1";
16
16
  const ROUTE_PREFIX = "/holdem";
17
17
  const MAX_SEATS = 6;
18
- const MAX_SPECTATORS = 16;
18
+ /** 观战席位默认值;实际值由设置项 maxSpectators 决定。 */
19
+ const DEFAULT_MAX_SPECTATORS = 16;
20
+ /** 设置项 maxSpectators 能配到的最大值。 */
21
+ const SPECTATORS_LIMIT = 64;
19
22
  const MAX_ROOMS = 8;
20
23
  const MAX_CHAT_LINES = 80;
21
24
  const MAX_HISTORY_HANDS = 50;
@@ -24,6 +27,8 @@ const MAX_FRAME_BYTES = 64 * 1024;
24
27
  const TICK_MS = 250;
25
28
  const SESSION_TTL_MS = 1440 * 60 * 1e3;
26
29
  const DISCONNECT_AFTER_MS = 45e3;
30
+ /** 没有实时通道时,HTTP 请求也算「人在」:这么久没动静才标成离线。 */
31
+ const PRESENCE_TTL_MS = 2e4;
27
32
  const LOBBY_KICK_IDLE_MS = 600 * 1e3;
28
33
  const CHAT_COOLDOWN_MS = 1200;
29
34
  const BUY_IN_BB = 100;
@@ -66,6 +71,7 @@ const Config = z.object({
66
71
  turnTimeoutSec: z.number().default(30).min(15).max(90),
67
72
  autoAdvanceSec: z.number().default(6).min(2).max(30),
68
73
  allowSpectators: z.boolean().default(true),
74
+ maxSpectators: z.number().default(DEFAULT_MAX_SPECTATORS).min(0).max(SPECTATORS_LIMIT),
69
75
  chatEnabled: z.boolean().default(true),
70
76
  maxRooms: z.number().default(MAX_ROOMS).min(1).max(64),
71
77
  hostIdleDissolveMin: z.number().default(10).min(1).max(120),
@@ -90,6 +96,7 @@ function resolveConfig(config) {
90
96
  turnTimeoutMs: turnTimeoutSec * 1e3,
91
97
  autoAdvanceMs: autoAdvanceSec * 1e3,
92
98
  allowSpectators: config.allowSpectators !== false,
99
+ maxSpectators: clamp(Math.trunc(config.maxSpectators ?? DEFAULT_MAX_SPECTATORS), 0, SPECTATORS_LIMIT),
93
100
  chatEnabled: config.chatEnabled !== false,
94
101
  maxRooms: clamp(Math.trunc(config.maxRooms ?? MAX_ROOMS), 1, 64),
95
102
  hostIdleDissolveMs: clamp(config.hostIdleDissolveMin ?? 10, 1, 120) * 6e4,
@@ -1446,7 +1453,7 @@ function traceWs(line) {
1446
1453
  function registerHoldemSocket(server, manager) {
1447
1454
  const wss = new WebSocketServer({
1448
1455
  noServer: true,
1449
- handleProtocols: (protocols) => protocols.has(SUBPROTOCOL) ? SUBPROTOCOL : false
1456
+ handleProtocols: (protocols) => protocols.has(SUBPROTOCOL) ? SUBPROTOCOL : ""
1450
1457
  });
1451
1458
  const sockets = /* @__PURE__ */ new Map();
1452
1459
  const off = manager.onEvent((playerId, event) => {
@@ -3086,6 +3093,7 @@ var RoomManager = class {
3086
3093
  turnTimeoutMs: this.config.turnTimeoutMs,
3087
3094
  autoAdvanceMs: this.config.autoAdvanceMs,
3088
3095
  allowSpectators: this.config.allowSpectators,
3096
+ maxSpectators: this.config.maxSpectators,
3089
3097
  chatEnabled: this.config.chatEnabled
3090
3098
  },
3091
3099
  players: /* @__PURE__ */ new Map(),
@@ -3168,8 +3176,8 @@ var RoomManager = class {
3168
3176
  if (cookieSession) {
3169
3177
  const player$1 = room.players.get(cookieSession.playerId);
3170
3178
  if (player$1 && !player$1.leaving) {
3171
- player$1.connected = true;
3172
3179
  player$1.browserId = input.browserId ?? player$1.browserId;
3180
+ this.touch(room, player$1);
3173
3181
  return {
3174
3182
  playerId: player$1.playerId,
3175
3183
  roomId: room.roomId,
@@ -3184,8 +3192,8 @@ var RoomManager = class {
3184
3192
  const seatedCount = [...room.players.values()].filter((player$1) => player$1.seat !== null).length;
3185
3193
  const canSit = wanted === "sit" && invite.sit && room.phase === "lobby" && seatedCount < MAX_SEATS;
3186
3194
  if (!canSit) {
3187
- if (!room.settings.allowSpectators) throw new CommandError("room-full", "房间已满且不允许观战");
3188
- if ([...room.players.values()].filter((player$1) => player$1.seat === null).length >= MAX_SPECTATORS) throw new CommandError("room-full", "观战席已满");
3195
+ if (!room.settings.allowSpectators || room.settings.maxSpectators <= 0) throw new CommandError("room-full", "房间已满且不允许观战");
3196
+ if ([...room.players.values()].filter((player$1) => player$1.seat === null).length >= room.settings.maxSpectators) throw new CommandError("room-full", "观战席已满");
3189
3197
  if (wanted === "sit" && invite.sit === false) throw new CommandError("auth", "没有入座权限");
3190
3198
  }
3191
3199
  const seat = canSit ? this.firstFreeSeat(room) : null;
@@ -3260,7 +3268,8 @@ var RoomManager = class {
3260
3268
  view(roomId, playerId) {
3261
3269
  const room = this.roomOf(playerId);
3262
3270
  if (!room || room.roomId !== roomId) throw new CommandError("auth", "不在这个房间里");
3263
- if (room.hostPlayerId === playerId) room.hostLastSeen = this.now();
3271
+ const player = room.players.get(playerId);
3272
+ if (player) this.touch(room, player);
3264
3273
  return this.project(room, playerId);
3265
3274
  }
3266
3275
  command(roomId, playerId, command, baseSeq, actionId) {
@@ -3268,7 +3277,7 @@ var RoomManager = class {
3268
3277
  if (!room || room.roomId !== roomId) throw new CommandError("auth", "不在这个房间里");
3269
3278
  const player = room.players.get(playerId);
3270
3279
  if (!player || player.leaving) throw new CommandError("auth", "不在这个房间里");
3271
- if (room.hostPlayerId === playerId) room.hostLastSeen = this.now();
3280
+ this.touch(room, player);
3272
3281
  if (command.type === "ping") return {
3273
3282
  seq: room.seq,
3274
3283
  view: this.project(room, playerId)
@@ -3411,13 +3420,32 @@ var RoomManager = class {
3411
3420
  roomId: entry.roomId
3412
3421
  };
3413
3422
  }
3423
+ /**
3424
+ * 实时通道打开/关闭。
3425
+ *
3426
+ * 关掉 socket 不等于立刻离线:后台轮询(HTTP)还活着的话人还在。真正判离线交给
3427
+ * `tick` 按 `PRESENCE_TTL_MS` 扫一遍,两边都没有动静才标成离线。
3428
+ */
3414
3429
  markConnected(playerId, roomId, connected) {
3415
3430
  const room = this.rooms.get(roomId);
3416
3431
  const player = room?.players.get(playerId);
3417
3432
  if (!room || !player) return;
3418
- if (room.hostPlayerId === playerId && connected) room.hostLastSeen = this.now();
3419
- if (player.connected === connected) return;
3420
- player.connected = connected;
3433
+ player.socketOpen = connected;
3434
+ if (connected) player.lastSeenAt = this.now();
3435
+ this.refreshPresence(room, player);
3436
+ }
3437
+ /** HTTP 请求也算心跳:轮询模式下不能把自己和别人都显示成离线。 */
3438
+ touch(room, player) {
3439
+ if (player.leaving) return;
3440
+ player.lastSeenAt = this.now();
3441
+ if (room.hostPlayerId === player.playerId) room.hostLastSeen = player.lastSeenAt;
3442
+ this.refreshPresence(room, player);
3443
+ }
3444
+ /** 有实时通道,或者最近有 HTTP 动静,就算在线。 */
3445
+ refreshPresence(room, player) {
3446
+ const online = !player.leaving && (player.socketOpen || this.now() - player.lastSeenAt < PRESENCE_TTL_MS);
3447
+ if (player.connected === online) return;
3448
+ player.connected = online;
3421
3449
  this.publish(room);
3422
3450
  }
3423
3451
  health() {
@@ -3576,6 +3604,7 @@ var RoomManager = class {
3576
3604
  if (room.phase === "hand" && room.hand) this.tickHand(room, now);
3577
3605
  if (room.phase === "handOver" && room.handOverAt !== null && now >= room.handOverAt) this.advance(room, now);
3578
3606
  for (const player of [...room.players.values()]) if (player.leaving && room.phase !== "hand") this.removePlayer(room, player.playerId);
3607
+ else this.refreshPresence(room, player);
3579
3608
  }
3580
3609
  this.cleanupSessions(now);
3581
3610
  }
@@ -3726,6 +3755,8 @@ var RoomManager = class {
3726
3755
  seat,
3727
3756
  ready: false,
3728
3757
  connected: true,
3758
+ socketOpen: false,
3759
+ lastSeenAt: this.now(),
3729
3760
  sittingOut: false,
3730
3761
  autoFold: false,
3731
3762
  timeoutStreak: 0,