dou-cl 1.0.0 → 1.0.2

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/src/net/server.js CHANGED
@@ -1,5 +1,18 @@
1
1
  'use strict';
2
2
 
3
+ /*
4
+ * 联网服务端 —— **一个端口就是一个房间**。
5
+ *
6
+ * npx dou-cl --serve --port 8080 -g doudizhu
7
+ *
8
+ * 服务器自己不占座位,只是托管这个房间:谁连上 127.0.0.1:8080 就坐进来。
9
+ * · 人满 → 回一句 full,客户端打印提示即可
10
+ * · 人不够 → 等着(可以一直等),玩家随时能退出,也能敲一下用电脑补齐先开局
11
+ * · 人够了 → 自动开局
12
+ *
13
+ * 想一口气开一片房间(公网场景):--ports 10000-12000,每个端口一个房间。
14
+ */
15
+
3
16
  const WebSocket = require('ws');
4
17
  const gamesMod = require('../games');
5
18
  const rulesMod = require('../core/rules');
@@ -8,13 +21,10 @@ const runner = require('../runner');
8
21
 
9
22
  const sleep = runner.sleep;
10
23
 
11
- const CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
12
-
13
- function makeCode() {
14
- let s = '';
15
- for (let i = 0; i < 4; i++) s += CODE_CHARS[Math.floor(Math.random() * CODE_CHARS.length)];
16
- return s;
17
- }
24
+ /** 一局打完,留多久让大家看清结果再回大厅 */
25
+ const RESULT_HOLD_MS = 4000;
26
+ /** 人够了以后,等这么久再自动开局(给人一点反应时间) */
27
+ const AUTO_START_DELAY_MS = 1200;
18
28
 
19
29
  function send(ws, obj) {
20
30
  if (ws && ws.readyState === 1) {
@@ -22,86 +32,92 @@ function send(ws, obj) {
22
32
  }
23
33
  }
24
34
 
35
+ /* ------------------------------------------------------------------ */
36
+ /* 房间 */
37
+ /* ------------------------------------------------------------------ */
38
+
25
39
  class Room {
26
- constructor(key, opts) {
27
- this.code = makeCode();
28
- this.gameKey = key;
29
- this.rules = rulesMod.get(key);
40
+ constructor(gameKey, opts) {
41
+ opts = opts || {};
42
+ this.gameKey = gameKey;
43
+ this.rules = rulesMod.get(gameKey);
30
44
  this.seatCount = this.rules.seats;
31
- this.members = []; // 下标即座位号
32
- this.spectators = [];
33
- this.hostId = null;
34
- this.public = !!(opts && opts.public);
35
- this.started = false;
45
+ this.members = new Array(this.seatCount).fill(null); // 下标即座位号
46
+ this.started = false; // 正在打
47
+ this.cooling = false; // 刚打完,正在展示结果
36
48
  this.state = null;
37
49
  this.engine = null;
38
- this.botDelay = (opts && opts.botDelay) || 800;
39
- this.turnTimeout = (opts && opts.turnTimeout) || 90000;
40
- this.autoStartTimer = null;
41
- this.matchCountdown = (opts && opts.matchSeconds) || 12;
50
+ this.botDelay = opts.botDelay || 800;
51
+ this.turnTimeout = opts.turnTimeout || 90000;
52
+ this.startTimer = null;
53
+ }
54
+
55
+ humans() {
56
+ let n = 0;
57
+ for (let i = 0; i < this.seatCount; i++) if (this.members[i] && !this.members[i].bot) n++;
58
+ return n;
59
+ }
60
+
61
+ occupied() {
62
+ let n = 0;
63
+ for (let i = 0; i < this.seatCount; i++) if (this.members[i]) n++;
64
+ return n;
65
+ }
66
+
67
+ freeSeat() {
68
+ for (let i = 0; i < this.seatCount; i++) if (!this.members[i]) return i;
69
+ return -1;
42
70
  }
43
71
 
44
72
  info() {
45
- const self = this;
46
73
  const players = [];
47
- let filled = 0, total = 0;
48
74
  for (let i = 0; i < this.seatCount; i++) {
49
75
  const m = this.members[i];
50
- if (m && !m.bot) filled++;
51
- if (m) total++;
52
76
  players.push(m ? { name: m.name, bot: !!m.bot } : null);
53
77
  }
54
78
  return {
55
- code: this.code,
56
79
  game: this.gameKey,
57
80
  gameName: this.rules.name,
81
+ tagline: this.rules.tagline,
58
82
  seats: this.seatCount,
59
- filled: filled,
60
- total: total,
83
+ humans: this.humans(),
84
+ occupied: this.occupied(),
61
85
  started: this.started,
62
- public: this.public,
63
- hostSeat: this.members.findIndex(function (m) { return m && m.id === self.hostId; }),
64
86
  players: players,
65
87
  };
66
88
  }
67
-
68
- freeSeat() {
69
- for (let i = 0; i < this.seatCount; i++) if (!this.members[i]) return i;
70
- return -1;
71
- }
72
89
  }
73
90
 
74
- function createServer(opts) {
75
- opts = opts || {};
76
- const port = opts.port || 8080;
77
- const host = opts.host || '0.0.0.0';
78
- const rooms = new Map();
79
- const wss = new WebSocket.Server({ port: port, host: host });
91
+ /* ------------------------------------------------------------------ */
92
+ /* 单个端口的房间服务 */
93
+ /* ------------------------------------------------------------------ */
94
+
95
+ function startRoomServer(port, gameKey, opts, hooks) {
96
+ const room = new Room(gameKey, opts);
97
+ const wss = new WebSocket.Server({ port: port, host: opts.host || '0.0.0.0' });
98
+ hooks.servers.push(wss);
80
99
 
81
- function log(s) { if (!opts.quiet) console.log('[dou-cl] ' + s); }
100
+ /* ---- 广播 ---- */
82
101
 
83
- function broadcastRoomInfo(room) {
84
- const info = room.info();
85
- room.members.forEach(function (m) { if (m) send(m.ws, { t: 'room', room: info, youName: m.name }); });
86
- room.spectators.forEach(function (s) { send(s.ws, { t: 'room', room: info }); });
102
+ function broadcast(msg) {
103
+ room.members.forEach(function (m) { if (m) send(m.ws, msg); });
87
104
  }
88
105
 
89
- function broadcastState(room) {
106
+ function broadcastLobby() {
107
+ broadcast({ t: 'lobby', room: room.info() });
108
+ }
109
+
110
+ function broadcastState() {
90
111
  if (!room.state || !room.engine) return;
91
112
  room.members.forEach(function (m, i) {
92
113
  if (!m) return;
93
- send(m.ws, {
94
- t: 'state',
95
- view: room.engine.view(room.state, i),
96
- room: room.info(),
97
- });
98
- });
99
- room.spectators.forEach(function (s) {
100
- send(s.ws, { t: 'state', view: room.engine.view(room.state, -1), room: room.info() });
114
+ send(m.ws, { t: 'state', view: room.engine.view(room.state, i), room: room.info() });
101
115
  });
102
116
  }
103
117
 
104
- function makeDecider(room) {
118
+ /* ---- 谁该出牌 ---- */
119
+
120
+ function makeDecider() {
105
121
  return function (state, seat) {
106
122
  const m = room.members[seat];
107
123
  if (!m || m.bot || !m.ws || m.ws.readyState !== 1) {
@@ -115,14 +131,16 @@ function createServer(opts) {
115
131
  if (done) return;
116
132
  done = true;
117
133
  m.pending = null;
134
+ m.lastState = null;
118
135
  clearTimeout(m.timer);
119
136
  m.timer = null;
120
137
  resolve(action);
121
138
  };
122
139
  m.pending = finish;
140
+ m.lastState = state;
123
141
  m.timer = setTimeout(function () {
124
142
  if (m.pending === finish) {
125
- send(m.ws, { t: 'notice', msg: '超时,已自动帮你出牌' });
143
+ send(m.ws, { t: 'notice', msg: '超时,已自动帮你出牌', en: 'timeout, auto-played for you' });
126
144
  finish(bot.chooseAction(room.engine, state, seat));
127
145
  }
128
146
  }, room.turnTimeout);
@@ -130,303 +148,254 @@ function createServer(opts) {
130
148
  };
131
149
  }
132
150
 
133
- async function runRoom(room) {
134
- room.started = true;
151
+ /* ---- 打一局 ---- */
152
+
153
+ async function playOneRound() {
135
154
  const engine = gamesMod.create(room.gameKey);
136
155
  room.engine = engine;
137
- const players = [];
138
- for (let i = 0; i < room.seatCount; i++) {
139
- const m = room.members[i];
140
- players.push({ name: m ? m.name : ('电脑' + (i + 1)), bot: m ? !!m.bot : true });
141
- }
142
- const decider = makeDecider(room);
143
-
144
- broadcastRoomInfo(room);
145
- for (let round = 0; round < 200; round++) {
146
- let state;
147
- let tries = 0;
148
- do {
149
- state = await runner.runDeal(engine, players, decider,
150
- function (st) { room.state = st; broadcastState(room); },
151
- {});
152
- tries++;
153
- } while (state.result && state.result.type === 'redeal' && tries < 5);
154
-
155
- room.state = state;
156
- broadcastState(room);
157
-
158
- // 等房主决定是否再来一局
159
- const host = room.members.find(function (m) { return m && m.id === room.hostId && !m.bot; });
160
- if (!host || host.ws.readyState !== 1) break;
161
- const again = await new Promise(function (resolve) {
162
- let done = false;
163
- const finish = function (v) {
164
- if (done) return;
165
- done = true;
166
- host.pendingAgain = null;
167
- clearTimeout(host.timerAgain);
168
- resolve(v);
169
- };
170
- host.pendingAgain = finish;
171
- host.timerAgain = setTimeout(function () { finish(false); }, 120000);
172
- });
173
- if (!again) break;
156
+ const players = room.members.map(function (m, i) {
157
+ return { name: m ? m.name : ('电脑' + (i + 1)), bot: m ? !!m.bot : true };
158
+ });
159
+ const decider = makeDecider();
160
+
161
+ let state;
162
+ let tries = 0;
163
+ do {
164
+ state = await runner.runDeal(engine, players, decider,
165
+ function (st) { room.state = st; broadcastState(); }, {});
166
+ tries++;
167
+ } while (state.result && state.result.type === 'redeal' && tries < 5);
168
+
169
+ room.state = state;
170
+ broadcastState();
171
+ return state;
172
+ }
173
+
174
+ async function runRoom() {
175
+ if (room.started || room.cooling) return;
176
+ room.started = true;
177
+ clearTimeout(room.startTimer);
178
+ room.startTimer = null;
179
+ try {
180
+ await playOneRound();
181
+ } catch (e) {
182
+ hooks.log('端口 ' + port + ' 的房间出错: ' + (e && e.message));
174
183
  }
175
184
  room.started = false;
176
185
  room.state = null;
177
- broadcastRoomInfo(room);
178
- room.members.forEach(function (m) { if (m && !m.bot) send(m.ws, { t: 'lobby', room: room.info() }); });
179
- // 清掉机器人和已断线的人,从大厅重新开始
180
- for (let i = 0; i < room.seatCount; i++) {
181
- const m = room.members[i];
182
- if (m && m.bot) room.members[i] = null;
183
- }
184
- if (!room.members.some(Boolean) && !room.spectators.length) rooms.delete(room.code);
186
+ room.engine = null;
187
+
188
+ // 先把结果留在屏幕上,再回大厅
189
+ room.cooling = true;
190
+ setTimeout(function () {
191
+ room.cooling = false;
192
+ for (let i = 0; i < room.seatCount; i++) {
193
+ const m = room.members[i];
194
+ if (m && (m.bot || m.gone)) room.members[i] = null;
195
+ }
196
+ broadcastLobby();
197
+ maybeStart(2500);
198
+ }, RESULT_HOLD_MS);
185
199
  }
186
200
 
187
- function startRoom(room) {
188
- if (room.started) return;
189
- if (room.busy) return;
190
- room.busy = true;
191
- runRoom(room).catch(function (e) {
192
- log('房间 ' + room.code + ' 出错: ' + (e && e.message));
193
- room.started = false;
194
- room.busy = false;
195
- room.members.forEach(function (m) { if (m) send(m.ws, { t: 'error', msg: '对局异常: ' + (e && e.message) }); });
196
- }).then(function () { room.busy = false; });
201
+ function maybeStart(delay) {
202
+ if (room.started || room.cooling) return;
203
+ if (room.humans() <= 0 || room.humans() < room.seatCount) return;
204
+ clearTimeout(room.startTimer);
205
+ room.startTimer = setTimeout(function () {
206
+ room.startTimer = null;
207
+ if (room.started || room.cooling) return;
208
+ if (room.humans() <= 0 || room.humans() < room.seatCount) return;
209
+ runRoom();
210
+ }, delay || 0);
197
211
  }
198
212
 
199
- function findOpenRoom(key) {
200
- let best = null;
201
- rooms.forEach(function (r) {
202
- if (!r.public || r.started || r.gameKey !== key) return;
203
- if (r.freeSeat() < 0) return;
204
- if (!best) best = r;
205
- });
206
- return best;
213
+ function seatOf(ws) {
214
+ for (let i = 0; i < room.seatCount; i++) {
215
+ if (room.members[i] && room.members[i].ws === ws) return i;
216
+ }
217
+ return -1;
207
218
  }
208
219
 
209
- function scheduleAutoStart(room) {
210
- if (room.autoStartTimer) return;
211
- let left = room.matchCountdown;
212
- const tick = function () {
213
- if (room.started) { room.autoStartTimer = null; return; }
214
- const humans = room.members.filter(function (m) { return m && !m.bot; }).length;
215
- if (humans >= room.seatCount) { room.autoStartTimer = null; startRoom(room); return; }
216
- if (left <= 0) {
217
- room.autoStartTimer = null;
218
- startRoom(room);
219
- return;
220
- }
221
- room.members.forEach(function (m) {
222
- if (m && !m.bot) send(m.ws, { t: 'notice', msg: '匹配中… ' + left + ' 秒后开始(不足的座位由电脑补上)' });
223
- });
224
- room.spectators.forEach(function (s) {
225
- send(s.ws, { t: 'notice', msg: '匹配中… ' + left + ' 秒后开始' });
220
+ /* ---- 入座 / 离开 ---- */
221
+
222
+ function onJoin(ws, name) {
223
+ if (ws.seat !== undefined && ws.seat >= 0 && room.members[ws.seat] && room.members[ws.seat].ws === ws) {
224
+ return send(ws, { t: 'seated', seat: ws.seat, room: room.info() });
225
+ }
226
+ if (room.started || room.cooling) {
227
+ return send(ws, {
228
+ t: 'full',
229
+ msg: '房间正在游戏中(' + room.seatCount + '/' + room.seatCount + ' 人),稍后再试',
230
+ en: 'game in progress, try again later',
226
231
  });
227
- left -= 3;
228
- room.autoStartTimer = setTimeout(tick, 3000);
229
- };
230
- room.autoStartTimer = setTimeout(tick, 3000);
232
+ }
233
+ const seat = room.freeSeat();
234
+ if (seat < 0) {
235
+ return send(ws, { t: 'full', msg: '房间已满(' + room.seatCount + '/' + room.seatCount + ' 人)', en: 'room full' });
236
+ }
237
+ const clean = String(name || '').trim().slice(0, 12) || ('玩家' + (seat + 1));
238
+ room.members[seat] = { name: clean, bot: false, ws: ws, pending: null, gone: false };
239
+ ws.seat = seat;
240
+ send(ws, { t: 'seated', seat: seat, room: room.info() });
241
+ hooks.log(clean + ' 进入房间(端口 ' + port + ',座位 ' + seat + ')');
242
+ broadcastLobby();
243
+ maybeStart(AUTO_START_DELAY_MS);
231
244
  }
232
245
 
233
- function leaveRoom(ws) {
234
- rooms.forEach(function (room, code) {
235
- const idx = room.members.findIndex(function (m) { return m && m.ws === ws; });
236
- if (idx >= 0) {
237
- const m = room.members[idx];
238
- if (room.started) {
239
- // 游戏中断线 → 交给电脑托管
240
- m.bot = true;
241
- m.name = m.name + '(托管)';
242
- m.pending = null;
243
- m.pendingAgain = null;
244
- broadcastRoomInfo(room);
245
- } else {
246
- room.members[idx] = null;
247
- if (!room.members.some(Boolean) && !room.spectators.length) {
248
- clearTimeout(room.autoStartTimer);
249
- rooms.delete(code);
250
- } else {
251
- if (room.hostId === m.id) {
252
- const nx = room.members.find(function (x) { return x && !x.bot; });
253
- room.hostId = nx ? nx.id : null;
254
- }
255
- broadcastRoomInfo(room);
256
- }
257
- }
258
- }
259
- const si = room.spectators.findIndex(function (s) { return s.ws === ws; });
260
- if (si >= 0) {
261
- room.spectators.splice(si, 1);
262
- broadcastRoomInfo(room);
246
+ function onStart(ws) {
247
+ const seat = seatOf(ws);
248
+ if (seat < 0) return send(ws, { t: 'error', msg: '你还没有入座', en: 'not seated' });
249
+ if (room.started || room.cooling) return;
250
+ const who = room.members[seat].name;
251
+ broadcast({ t: 'notice', msg: who + ' 发起开局,不足的位置由电脑补上', en: 'start called, bots fill the empty seats' });
252
+ runRoom();
253
+ }
254
+
255
+ function onLeave(ws) {
256
+ const seat = seatOf(ws);
257
+ if (seat < 0) return;
258
+ const m = room.members[seat];
259
+
260
+ if (room.started) {
261
+ // 打牌中途走了 → 电脑接管这个座位,牌局继续
262
+ if (m.pending) {
263
+ const finish = m.pending;
264
+ m.pending = null;
265
+ clearTimeout(m.timer);
266
+ m.timer = null;
267
+ finish(bot.chooseAction(room.engine, m.lastState, seat));
263
268
  }
264
- });
269
+ m.bot = true;
270
+ m.gone = true;
271
+ m.ws = null;
272
+ m.name = m.name + '(托管)';
273
+ broadcast({ t: 'notice', msg: m.name + ' 断线,由电脑托管', en: 'a player left, autopilot on' });
274
+ hooks.log(m.name + ' 断线(端口 ' + port + '),由电脑托管');
275
+ return;
276
+ }
277
+
278
+ clearTimeout(room.startTimer);
279
+ room.startTimer = null;
280
+ room.members[seat] = null;
281
+ ws.seat = -1;
282
+ hooks.log((m && m.name ? m.name : '玩家') + ' 离开房间(端口 ' + port + ')');
283
+ broadcastLobby();
284
+ broadcast({ t: 'notice', msg: (m && m.name ? m.name : '玩家') + ' 离开了房间', en: 'a player left the room' });
265
285
  }
266
286
 
287
+ /* ---- 连接 ---- */
288
+
267
289
  wss.on('connection', function (ws) {
268
- ws.id = Math.random().toString(36).slice(2, 10);
269
290
  ws.isAlive = true;
291
+ ws.seat = -1;
270
292
  ws.on('pong', function () { ws.isAlive = true; });
271
293
 
272
- send(ws, { t: 'hello', id: ws.id, games: gamesMod.keys.map(function (k) { return { key: k, name: rulesMod.get(k).name, tagline: rulesMod.get(k).tagline, seats: rulesMod.get(k).seats }; }) });
294
+ send(ws, {
295
+ t: 'hello',
296
+ port: port,
297
+ game: room.gameKey,
298
+ gameName: room.rules.name,
299
+ tagline: room.rules.tagline,
300
+ seats: room.seatCount,
301
+ });
273
302
 
274
303
  ws.on('message', function (raw) {
275
304
  let m;
276
- try { m = JSON.parse(String(raw)); } catch (e) { return send(ws, { t: 'error', msg: '消息格式错误' }); }
277
-
278
- if (m.t === 'list') {
279
- const list = [];
280
- rooms.forEach(function (r) {
281
- if (r.started) return;
282
- if (r.freeSeat() < 0) return;
283
- list.push(r.info());
284
- });
285
- return send(ws, { t: 'rooms', rooms: list });
286
- }
305
+ try { m = JSON.parse(String(raw)); }
306
+ catch (e) { return send(ws, { t: 'error', msg: '消息格式错误', en: 'bad message' }); }
287
307
 
288
- if (m.t === 'create') {
289
- if (!rulesMod.get(m.game)) return send(ws, { t: 'error', msg: '未知玩法' });
290
- const room = new Room(m.game, {
291
- public: !!m.public,
292
- matchSeconds: opts.matchSeconds,
293
- botDelay: opts.botDelay,
294
- turnTimeout: opts.turnTimeout,
295
- });
296
- while (rooms.has(room.code)) room.code = makeCode();
297
- rooms.set(room.code, room);
298
- const seat = 0;
299
- room.members[seat] = { id: ws.id, ws: ws, name: m.name || '房主', bot: false, pending: null };
300
- room.hostId = ws.id;
301
- ws.roomCode = room.code;
302
- send(ws, { t: 'joined', code: room.code, seat: seat, host: true, room: room.info() });
303
- log('创建房间 ' + room.code + '(' + room.rules.name + ')by ' + (m.name || '房主'));
304
- broadcastRoomInfo(room);
305
- if (room.public) scheduleAutoStart(room);
306
- return;
307
- }
308
+ if (m.t === 'join') return onJoin(ws, m.name);
309
+ if (m.t === 'start') return onStart(ws);
310
+ if (m.t === 'leave') return onLeave(ws);
308
311
 
309
- if (m.t === 'join') {
310
- const code = String(m.code || '').toUpperCase();
311
- const room = rooms.get(code);
312
- if (!room) return send(ws, { t: 'error', msg: '房间 ' + code + ' 不存在' });
313
- const seat = room.freeSeat();
314
- if (seat < 0 && !room.started) return send(ws, { t: 'error', msg: '房间已满' });
315
- if (room.started) {
316
- room.spectators.push({ id: ws.id, ws: ws, name: m.name || '观众' });
317
- ws.roomCode = room.code;
318
- send(ws, { t: 'joined', code: room.code, seat: -1, spectator: true, room: room.info() });
319
- broadcastState(room);
320
- return;
321
- }
322
- room.members[seat] = { id: ws.id, ws: ws, name: m.name || ('玩家' + (seat + 1)), bot: false, pending: null };
323
- ws.roomCode = room.code;
324
- send(ws, { t: 'joined', code: room.code, seat: seat, host: room.hostId === ws.id, room: room.info() });
325
- log((m.name || '玩家') + ' 加入房间 ' + room.code + '(座位 ' + seat + ')');
326
- broadcastRoomInfo(room);
327
- if (room.public) scheduleAutoStart(room);
328
- return;
329
- }
330
-
331
- if (m.t === 'match') {
332
- const key = m.game;
333
- if (!rulesMod.get(key)) return send(ws, { t: 'error', msg: '未知玩法' });
334
- let room = findOpenRoom(key);
335
- if (!room) {
336
- room = new Room(key, {
337
- public: true,
338
- matchSeconds: opts.matchSeconds,
339
- botDelay: opts.botDelay,
340
- turnTimeout: opts.turnTimeout,
341
- });
342
- while (rooms.has(room.code)) room.code = makeCode();
343
- rooms.set(room.code, room);
344
- const seat0 = 0;
345
- room.members[seat0] = { id: ws.id, ws: ws, name: m.name || '玩家1', bot: false, pending: null };
346
- room.hostId = ws.id;
347
- ws.roomCode = room.code;
348
- send(ws, { t: 'joined', code: room.code, seat: seat0, host: true, room: room.info() });
349
- log('匹配新建房间 ' + room.code + '(' + room.rules.name + ')');
350
- } else {
351
- const seat = room.freeSeat();
352
- room.members[seat] = { id: ws.id, ws: ws, name: m.name || ('玩家' + (seat + 1)), bot: false, pending: null };
353
- ws.roomCode = room.code;
354
- send(ws, { t: 'joined', code: room.code, seat: seat, host: false, room: room.info() });
355
- log((m.name || '玩家') + ' 匹配进房间 ' + room.code);
356
- }
357
- broadcastRoomInfo(room);
358
- scheduleAutoStart(room);
359
- return;
360
- }
361
-
362
- const room = rooms.get(ws.roomCode);
363
- if (!room) return send(ws, { t: 'error', msg: '你还没有加入房间' });
364
- const mySeat = room.members.findIndex(function (x) { return x && x.ws === ws; });
365
-
366
- if (m.t === 'start') {
367
- if (ws.id !== room.hostId) return send(ws, { t: 'error', msg: '只有房主可以开始' });
368
- if (room.started) return;
369
- if (!room.members.some(Boolean)) return send(ws, { t: 'error', msg: '房间里没人' });
370
- return startRoom(room);
371
- }
372
-
373
- if (m.t === 'again') {
374
- const mm = room.members.find(function (x) { return x && x.id === ws.id; });
375
- if (mm && mm.pendingAgain) mm.pendingAgain(m.yes !== false);
376
- return;
377
- }
312
+ const seat = seatOf(ws);
313
+ if (seat < 0) return send(ws, { t: 'error', msg: '你还没有入座', en: 'not seated' });
378
314
 
379
315
  if (m.t === 'act') {
380
- if (mySeat < 0) return;
381
- const mm = room.members[mySeat];
382
- if (!mm || !mm.pending) return send(ws, { t: 'error', msg: '现在不该你操作' });
383
- const p = mm.pending;
316
+ const mm = room.members[seat];
317
+ if (!mm || !mm.pending) return send(ws, { t: 'error', msg: '现在不该你操作', en: 'not your turn' });
318
+ const finish = mm.pending;
384
319
  mm.pending = null;
385
- return p(m.action);
320
+ return finish(m.action);
386
321
  }
387
322
 
388
323
  if (m.t === 'chat') {
389
- const who = mySeat >= 0 && room.members[mySeat] ? room.members[mySeat].name : '观众';
390
- room.members.forEach(function (x) { if (x) send(x.ws, { t: 'chat', who: who, msg: String(m.msg).slice(0, 200) }); });
391
- room.spectators.forEach(function (s) { send(s.ws, { t: 'chat', who: who, msg: String(m.msg).slice(0, 200) }); });
324
+ const who = (room.members[seat] && room.members[seat].name) || '玩家';
325
+ broadcast({ t: 'chat', who: who, msg: String(m.msg).slice(0, 200) });
392
326
  return;
393
327
  }
394
-
395
- if (m.t === 'leave') {
396
- ws.roomCode = null;
397
- return leaveRoom(ws);
398
- }
399
328
  });
400
329
 
401
- ws.on('close', function () {
402
- leaveRoom(ws);
403
- });
330
+ ws.on('close', function () { onLeave(ws); });
331
+ ws.on('error', function () { /* ignore,交给 close */ });
332
+ });
333
+
334
+ wss.on('error', function (e) {
335
+ hooks.log('端口 ' + port + ' 启动失败: ' + e.message);
404
336
  });
405
337
 
338
+ return {
339
+ port: port,
340
+ room: room,
341
+ wss: wss,
342
+ close: function () { try { wss.close(); } catch (e) { /* ignore */ } },
343
+ };
344
+ }
345
+
346
+ /* ------------------------------------------------------------------ */
347
+ /* 对外 */
348
+ /* ------------------------------------------------------------------ */
349
+
350
+ function createServers(opts) {
351
+ opts = opts || {};
352
+ const ports = (opts.ports && opts.ports.length) ? opts.ports.slice()
353
+ : [opts.port || 8080];
354
+ const gameKey = opts.game || 'doudizhu';
355
+ if (!rulesMod.get(gameKey)) throw new Error('未知玩法:' + gameKey);
356
+
357
+ const log = opts.quiet ? function () {} : function (s) { console.log('[dou-cl] ' + s); };
358
+ const hooks = { servers: [], log: log };
359
+
360
+ const list = [];
361
+ for (let i = 0; i < ports.length; i++) {
362
+ list.push(startRoomServer(ports[i], gameKey, opts, hooks));
363
+ }
364
+
365
+ // 所有房间共用一条心跳
406
366
  const heartbeat = setInterval(function () {
407
- wss.clients.forEach(function (ws) {
408
- if (ws.isAlive === false) return ws.terminate();
409
- ws.isAlive = false;
410
- try { ws.ping(); } catch (e) { /* ignore */ }
367
+ hooks.servers.forEach(function (wss) {
368
+ wss.clients.forEach(function (ws) {
369
+ if (ws.isAlive === false) return ws.terminate();
370
+ ws.isAlive = false;
371
+ try { ws.ping(); } catch (e) { /* ignore */ }
372
+ });
411
373
  });
412
374
  }, 30000);
413
- wss.on('close', function () { clearInterval(heartbeat); });
414
375
 
415
- wss.on('listening', function () {
416
- const addr = wss.address();
417
- log('斗地主服务器已启动 端口 ' + addr.port);
418
- log('局域网内的玩家用: npx dou-cl --join <本机IP>:' + addr.port);
419
- });
420
- wss.on('error', function (e) {
421
- log('服务器错误: ' + e.message);
422
- });
376
+ const rules = rulesMod.get(gameKey);
377
+ if (ports.length === 1) {
378
+ log(rules.name + ' 房间已开 端口 ' + ports[0]);
379
+ log('别人连进来: npx dou-cl --join <本机IP>:' + ports[0]);
380
+ } else {
381
+ log(rules.name + ' 房间 × ' + ports.length + ' 端口 ' + ports[0] + '-' + ports[ports.length - 1]);
382
+ log('别人连进来: npx dou-cl --join <本机IP>:<端口>(每个端口一个独立房间)');
383
+ }
423
384
 
424
385
  return {
425
- wss: wss,
426
- port: port,
427
- rooms: rooms,
428
- close: function () { clearInterval(heartbeat); wss.close(); },
386
+ ports: ports,
387
+ game: gameKey,
388
+ rules: rules,
389
+ servers: list,
390
+ close: function () {
391
+ clearInterval(heartbeat);
392
+ list.forEach(function (s) { s.close(); });
393
+ },
429
394
  };
430
395
  }
431
396
 
432
- module.exports = { createServer: createServer };
397
+ module.exports = {
398
+ createServers: createServers,
399
+ createServer: createServers, // 兼容旧调用:opts.port 走单端口
400
+ Room: Room,
401
+ };