dou-cl 1.0.1 → 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/PUBLISH.md +3 -3
- package/README.md +7 -4
- package/bin/dou-cl.js +50 -30
- package/package.json +1 -1
- package/src/app.js +30 -43
- package/src/net/client.js +268 -130
- package/src/net/server.js +278 -309
- package/src/ui.js +3 -3
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
27
|
-
|
|
28
|
-
this.gameKey =
|
|
29
|
-
this.rules = rulesMod.get(
|
|
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.
|
|
33
|
-
this.
|
|
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 =
|
|
39
|
-
this.turnTimeout =
|
|
40
|
-
this.
|
|
41
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
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
|
-
|
|
100
|
+
/* ---- 广播 ---- */
|
|
82
101
|
|
|
83
|
-
function
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
151
|
+
/* ---- 打一局 ---- */
|
|
152
|
+
|
|
153
|
+
async function playOneRound() {
|
|
135
154
|
const engine = gamesMod.create(room.gameKey);
|
|
136
155
|
room.engine = engine;
|
|
137
|
-
const players =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
188
|
-
if (room.started) return;
|
|
189
|
-
if (room.
|
|
190
|
-
room.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
room.started
|
|
194
|
-
room.
|
|
195
|
-
|
|
196
|
-
}
|
|
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
|
|
200
|
-
let
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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, {
|
|
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)); }
|
|
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 === '
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
320
|
+
return finish(m.action);
|
|
386
321
|
}
|
|
387
322
|
|
|
388
323
|
if (m.t === 'chat') {
|
|
389
|
-
const who =
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
log('
|
|
418
|
-
log('
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
log('
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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 = {
|
|
397
|
+
module.exports = {
|
|
398
|
+
createServers: createServers,
|
|
399
|
+
createServer: createServers, // 兼容旧调用:opts.port 走单端口
|
|
400
|
+
Room: Room,
|
|
401
|
+
};
|