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/PUBLISH.md +114 -7
- package/README.md +63 -185
- package/bin/dou-cl.js +50 -30
- package/package.json +3 -2
- 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/app.js
CHANGED
|
@@ -127,7 +127,7 @@ function askHuman(io, engine, state, seat, key) {
|
|
|
127
127
|
if (skin) printStealthHelp(); else printHelp(key);
|
|
128
128
|
return step();
|
|
129
129
|
}
|
|
130
|
-
if (low === 'skins' || low === 'skin'
|
|
130
|
+
if (low === 'skins' || low === 'skin') { printSkinHelp(); return step(); }
|
|
131
131
|
if (low === 'quit' || low === 'exit') {
|
|
132
132
|
if (!skin) io.say(colors.dim('已退出本局。'));
|
|
133
133
|
io.close();
|
|
@@ -258,11 +258,8 @@ function showMenu(io, opts) {
|
|
|
258
258
|
io.say(' ' + colors.bold(m[0]) + ' ' + m[2] + ' ' + colors.dim(m[3]));
|
|
259
259
|
});
|
|
260
260
|
io.say('');
|
|
261
|
-
io.say(' ' + colors.bold('联机'));
|
|
262
|
-
io.say(' ' + colors.bold('4') + '
|
|
263
|
-
colors.bold('5') + ' 加入房间');
|
|
264
|
-
io.say(' ' + colors.bold('6') + ' 快速匹配 ' +
|
|
265
|
-
colors.bold('7') + ' 本机开服务器');
|
|
261
|
+
io.say(' ' + colors.bold('联机') + colors.dim('(一个 ip:port 就是一个房间)'));
|
|
262
|
+
io.say(' ' + colors.bold('4') + ' 加入房间 ' + colors.bold('5') + ' 开一个房间');
|
|
266
263
|
io.say('');
|
|
267
264
|
io.say(' ' + colors.bold('0') + ' 退出');
|
|
268
265
|
io.say(' ' + '\u2500'.repeat(58));
|
|
@@ -277,14 +274,14 @@ function showMenu(io, opts) {
|
|
|
277
274
|
.catch(function (e) { io.say(colors.red('出错: ' + e.message)); showMenu(io, opts); });
|
|
278
275
|
return;
|
|
279
276
|
}
|
|
280
|
-
if (c === '4'
|
|
281
|
-
|
|
277
|
+
if (c === '4') {
|
|
278
|
+
joinFlow(io, opts).catch(function (e) {
|
|
282
279
|
io.say(colors.red('出错: ' + e.message));
|
|
283
280
|
showMenu(io, opts);
|
|
284
281
|
});
|
|
285
282
|
return;
|
|
286
283
|
}
|
|
287
|
-
if (c === '
|
|
284
|
+
if (c === '5') { startServerFlow(io, opts); return; }
|
|
288
285
|
io.say(colors.red('没有这个选项:' + c));
|
|
289
286
|
setTimeout(function () { showMenu(io, opts); }, 600);
|
|
290
287
|
});
|
|
@@ -296,50 +293,40 @@ function ask(io, prompt) {
|
|
|
296
293
|
});
|
|
297
294
|
}
|
|
298
295
|
|
|
299
|
-
|
|
296
|
+
/** 联机 · 加入房间:问昵称 + 地址,地址本身就是房间 */
|
|
297
|
+
async function joinFlow(io, opts) {
|
|
300
298
|
io.clear();
|
|
301
299
|
io.say('');
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (choice === '1') {
|
|
305
|
-
io.say('');
|
|
306
|
-
io.say(' 选择玩法: 1 斗地主 2 跑得快 3 510K');
|
|
307
|
-
const g = await ask(io, ' > ');
|
|
308
|
-
const key = ({ '1': 'doudizhu', '2': 'paodekuai', '3': 'fivek' })[g.trim()] || 'doudizhu';
|
|
309
|
-
const srv = await ask(io, ' 服务器地址(直接回车 = 本机 ' + opts.server + ') > ') || opts.server;
|
|
310
|
-
io.close();
|
|
311
|
-
const client = require('./net/client');
|
|
312
|
-
await client.runClient(new ui.IO(), { mode: 'create', game: key, name: name, server: srv }, opts);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (choice === '2') {
|
|
317
|
-
const code = await ask(io, ' 房间号(4 位) > ');
|
|
318
|
-
const srv = await ask(io, ' 服务器地址(直接回车 = 本机 ' + opts.server + ') > ') || opts.server;
|
|
319
|
-
io.close();
|
|
320
|
-
const client = require('./net/client');
|
|
321
|
-
await client.runClient(new ui.IO(), { mode: 'join', code: code.toUpperCase(), name: name, server: srv }, opts);
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
|
|
300
|
+
io.say(' ' + colors.bold('加入房间') + colors.dim(' 填对方的 ip:port 就行'));
|
|
325
301
|
io.say('');
|
|
326
|
-
io
|
|
327
|
-
const
|
|
328
|
-
const key = ({ '1': 'doudizhu', '2': 'paodekuai', '3': 'fivek' })[g.trim()] || 'doudizhu';
|
|
329
|
-
const srv = await ask(io, ' 服务器地址(直接回车 = 本机 ' + opts.server + ') > ') || opts.server;
|
|
302
|
+
const name = (await ask(io, ' 你的昵称 > ')) || '玩家';
|
|
303
|
+
const srv = (await ask(io, ' 房间地址 ip:port(回车 = ' + opts.server + ') > ')) || opts.server;
|
|
330
304
|
io.close();
|
|
331
305
|
const client = require('./net/client');
|
|
332
|
-
await client.runClient(new ui.IO(), {
|
|
306
|
+
await client.runClient(new ui.IO(), { server: srv, name: name }, opts);
|
|
333
307
|
}
|
|
334
308
|
|
|
335
|
-
|
|
309
|
+
/** 联机 · 本机开一个房间(服务器自己不占座位,别人用 --join 连进来) */
|
|
310
|
+
async function startServerFlow(io, opts) {
|
|
336
311
|
io.clear();
|
|
337
312
|
io.say('');
|
|
338
|
-
io.say('
|
|
313
|
+
io.say(' ' + colors.bold('开一个房间') + colors.dim(' 本机起一个房间,别人连进来'));
|
|
314
|
+
io.say('');
|
|
315
|
+
io.say(' 玩法: ' + colors.bold('1') + ' 斗地主 ' + colors.bold('2') + ' 跑得快 ' + colors.bold('3') + ' 510K');
|
|
316
|
+
const g = await ask(io, ' > ');
|
|
317
|
+
const key = ({ '1': 'doudizhu', '2': 'paodekuai', '3': 'fivek' })[g.trim()] || 'doudizhu';
|
|
318
|
+
const p = await ask(io, ' 端口(回车 = ' + opts.port + ') > ');
|
|
319
|
+
const port = Number(p) || opts.port;
|
|
320
|
+
|
|
321
|
+
const rules = rulesMod.get(key);
|
|
339
322
|
const server = require('./net/server');
|
|
340
|
-
server.
|
|
341
|
-
|
|
342
|
-
io.say(
|
|
323
|
+
server.createServers({ game: key, ports: [port] });
|
|
324
|
+
|
|
325
|
+
io.say('');
|
|
326
|
+
io.say(colors.dim(' 房间玩法:' + rules.name + ' 端口:' + port));
|
|
327
|
+
io.say(colors.dim(' 把地址发给朋友: npx dou-cl --join <你的IP>:' + port));
|
|
328
|
+
io.say(colors.dim(' 本机 IP 用 ipconfig 查;服务端自己不占座位。'));
|
|
329
|
+
io.say(colors.dim(' Ctrl+C 停止。'));
|
|
343
330
|
io.handler = null;
|
|
344
331
|
}
|
|
345
332
|
|
package/src/net/client.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/*
|
|
4
|
+
* 联网客户端 —— 连上一个地址就是进入那个房间。
|
|
5
|
+
*
|
|
6
|
+
* npx dou-cl --join 192.168.1.10:8080
|
|
7
|
+
*
|
|
8
|
+
* 规则:**只有「出牌」需要服务器**。
|
|
9
|
+
* 换皮肤 / reveal / redraw / 帮助 / 退出这些都是纯本地的,任何时刻都能用 ——
|
|
10
|
+
* 哪怕是别人的回合、你正在等他们出牌。
|
|
11
|
+
*/
|
|
12
|
+
|
|
3
13
|
const WebSocket = require('ws');
|
|
4
14
|
const ui = require('../ui');
|
|
5
15
|
const bot = require('../bot');
|
|
6
|
-
const gamesMod = require('../games');
|
|
7
16
|
const rulesMod = require('../core/rules');
|
|
17
|
+
const comboMod = require('../core/combo');
|
|
18
|
+
const skinsMod = require('../skins');
|
|
8
19
|
|
|
9
20
|
const colors = ui.colors;
|
|
10
21
|
|
|
22
|
+
/* ------------------------------------------------------------------ */
|
|
23
|
+
|
|
11
24
|
function hintIds(view, seat) {
|
|
12
25
|
const act = bot.hintFromView(view, seat);
|
|
13
26
|
if (act && act.type === 'play' && act.ids && act.ids.length) return act.ids;
|
|
@@ -18,245 +31,370 @@ function asciiName(s, fallback) {
|
|
|
18
31
|
return /^[\x20-\x7e]*$/.test(s || '') ? s : fallback;
|
|
19
32
|
}
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
function toUrl(s) {
|
|
35
|
+
let v = String(s || '').trim();
|
|
36
|
+
if (!v) v = '127.0.0.1:8080';
|
|
37
|
+
if (v.indexOf('ws://') === 0 || v.indexOf('wss://') === 0) return v;
|
|
38
|
+
if (v.indexOf(':') < 0) v += ':8080';
|
|
39
|
+
return 'ws://' + v;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ------------------------------------------------------------------ */
|
|
25
43
|
|
|
44
|
+
async function runClient(io, opts) {
|
|
45
|
+
const url = toUrl(opts.server);
|
|
46
|
+
const addr = url.replace(/^wss?:\/\//, '');
|
|
26
47
|
const skin0 = ui.skinActive();
|
|
27
|
-
|
|
28
|
-
|
|
48
|
+
|
|
49
|
+
if (skin0) io.block(ui.skinMessage('connect svc=deck-sync addr=' + url));
|
|
50
|
+
else io.say(colors.dim('连接中 ' + addr + ' …'));
|
|
29
51
|
|
|
30
52
|
const ws = new WebSocket(url);
|
|
31
53
|
let mySeat = -1;
|
|
32
54
|
let lastView = null;
|
|
55
|
+
let roomInfo = null;
|
|
56
|
+
let gameKey = opts.game || 'doudizhu';
|
|
57
|
+
let leaveSent = false;
|
|
58
|
+
let sigHandler = null;
|
|
33
59
|
|
|
34
60
|
function sendMsg(o) {
|
|
35
61
|
if (ws.readyState === 1) ws.send(JSON.stringify(o));
|
|
36
62
|
}
|
|
37
63
|
|
|
38
|
-
|
|
39
|
-
|
|
64
|
+
/** 摸鱼模式下只输出 ASCII —— 有 en 就用 en,避免漏出中文 */
|
|
65
|
+
function pick(msg, en) {
|
|
66
|
+
return (ui.skinActive() && en) ? en : msg;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function notify(msg, en) {
|
|
70
|
+
const body = pick(msg, en);
|
|
71
|
+
const m = ui.skinMessage(body);
|
|
40
72
|
if (m !== null) io.block(m);
|
|
41
|
-
else io.say(colors.dim('· ' +
|
|
73
|
+
else io.say(colors.dim('· ' + body));
|
|
42
74
|
}
|
|
43
75
|
|
|
44
|
-
function complain(msg) {
|
|
45
|
-
|
|
46
|
-
|
|
76
|
+
function complain(msg, en) {
|
|
77
|
+
const body = pick(msg, en);
|
|
78
|
+
if (ui.skinActive()) io.block(ui.skinMessage('! ' + body));
|
|
79
|
+
else io.say(colors.red(body));
|
|
47
80
|
}
|
|
48
81
|
|
|
49
|
-
function
|
|
82
|
+
function quit() {
|
|
83
|
+
if (leaveSent) return;
|
|
84
|
+
leaveSent = true;
|
|
85
|
+
sendMsg({ t: 'leave' });
|
|
86
|
+
try { ws.close(); } catch (e) { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isQuit(line) {
|
|
90
|
+
const low = (line || '').trim().toLowerCase();
|
|
91
|
+
return low === 'q' || low === 'quit' || low === 'exit' || low === 'leave';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ---- Ctrl+C:先说一声再走(别人 / 服务器立刻知道) ---- */
|
|
95
|
+
|
|
96
|
+
sigHandler = function () {
|
|
97
|
+
if (leaveSent) { process.exit(0); return; }
|
|
98
|
+
leaveSent = true;
|
|
99
|
+
sendMsg({ t: 'leave' });
|
|
100
|
+
// 给 socket 一点时间把这句话送出去,再退出
|
|
101
|
+
setTimeout(function () { process.exit(0); }, 150);
|
|
102
|
+
};
|
|
103
|
+
process.on('SIGINT', sigHandler);
|
|
104
|
+
|
|
105
|
+
function dropSigint() {
|
|
106
|
+
if (sigHandler) { process.removeListener('SIGINT', sigHandler); sigHandler = null; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ---- 帮助 ---- */
|
|
110
|
+
|
|
111
|
+
function printHelp(key) {
|
|
112
|
+
const r = rulesMod.get(key || gameKey);
|
|
113
|
+
const lines = [r.name + ' ' + r.tagline].concat(r.help || []);
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('出牌:34567 或 3 4 5 6 7 ;带花色 5s 5h 5c');
|
|
116
|
+
lines.push('p=不要 h=提示 ?=帮助 q=离开');
|
|
50
117
|
if (ui.skinActive()) {
|
|
51
|
-
|
|
52
|
-
return;
|
|
118
|
+
const s = ui.skinHelp(lines);
|
|
119
|
+
if (s !== null) { process.stdout.write(s); return; }
|
|
53
120
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
io.ask(prompt, function (line) { handleInput(line, view); });
|
|
121
|
+
io.say('');
|
|
122
|
+
lines.forEach(function (l) { io.say(' ' + l); });
|
|
123
|
+
io.say('');
|
|
58
124
|
}
|
|
59
125
|
|
|
60
|
-
|
|
61
|
-
|
|
126
|
+
/* ---- 纯本地命令:任何时刻都该能用 ---- */
|
|
127
|
+
|
|
128
|
+
/** @returns true 表示这条输入已经被本地处理掉,不要当作出牌 */
|
|
129
|
+
function localCommand(line, view) {
|
|
130
|
+
const low = (line || '').trim().toLowerCase();
|
|
62
131
|
const skin = ui.skinActive();
|
|
63
|
-
if (!line) { promptTurn(view); return; }
|
|
64
132
|
|
|
65
133
|
if (skin) {
|
|
66
134
|
if (low === 'reveal' || low === 'real') {
|
|
67
|
-
process.stdout.write(ui.renderTable(view, mySeat, {}) + '\n');
|
|
68
|
-
|
|
69
|
-
return;
|
|
135
|
+
process.stdout.write(ui.renderTable(view || lastView, mySeat, {}) + '\n');
|
|
136
|
+
return true;
|
|
70
137
|
}
|
|
71
138
|
if (low === 'redraw' || low === 'rr') {
|
|
72
|
-
io.block(ui.renderSkin(view, mySeat, {}));
|
|
73
|
-
|
|
74
|
-
return;
|
|
139
|
+
if (view || lastView) io.block(ui.renderSkin(view || lastView, mySeat, {}));
|
|
140
|
+
return true;
|
|
75
141
|
}
|
|
76
142
|
if (low.indexOf('skin') === 0) {
|
|
77
143
|
const k = low.split(/\s+/)[1];
|
|
78
144
|
if (!k) {
|
|
79
|
-
ui.skinList().
|
|
145
|
+
notify('skins: ' + ui.skinList().map(function (s) { return s.key; }).join(' '),
|
|
146
|
+
'skins: ' + ui.skinList().map(function (s) { return s.key; }).join(' '));
|
|
80
147
|
} else {
|
|
81
|
-
try { ui.setSkin(k); notify('skin -> ' + ui.skinKey()); }
|
|
82
|
-
catch (e) { notify(
|
|
148
|
+
try { ui.setSkin(k); notify('skin -> ' + ui.skinKey(), 'skin -> ' + ui.skinKey()); }
|
|
149
|
+
catch (e) { notify('no such skin: ' + k, 'no such skin: ' + k); }
|
|
83
150
|
}
|
|
84
|
-
|
|
85
|
-
return;
|
|
151
|
+
return true;
|
|
86
152
|
}
|
|
87
153
|
}
|
|
154
|
+
if (low === 'help' || low === '?' || low === 'rules') { printHelp(view && view.key); return true; }
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* ---- 等待界面 ---- */
|
|
159
|
+
|
|
160
|
+
function askWaiting() {
|
|
161
|
+
const skin = ui.skinActive();
|
|
162
|
+
const more = !roomInfo || roomInfo.humans < roomInfo.seats;
|
|
163
|
+
const hint = more ? '[s] 用电脑补齐先开局 [q] 退出' : '[q] 退出';
|
|
164
|
+
io.ask(skin ? ui.skinPrompt() : '\n' + colors.dim(hint) + ' > ', function (line) {
|
|
165
|
+
const low = (line || '').trim().toLowerCase();
|
|
166
|
+
if (isQuit(low)) { quit(); return; }
|
|
167
|
+
if (low === 's' || low === 'start') { sendMsg({ t: 'start' }); return; }
|
|
168
|
+
if (low === '?' || low === 'help') { printHelp(roomInfo && roomInfo.game); }
|
|
169
|
+
askWaiting();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function renderLobby(room) {
|
|
174
|
+
roomInfo = room;
|
|
175
|
+
gameKey = room.game;
|
|
176
|
+
const skin = ui.skinActive();
|
|
88
177
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
178
|
+
if (skin) {
|
|
179
|
+
notify('room=' + addr + ' svc=deck-sync game=' + room.game +
|
|
180
|
+
' seats=' + room.seats + ' ready=' + room.humans + '/' + room.seats);
|
|
181
|
+
const nodes = [];
|
|
182
|
+
for (let i = 0; i < room.seats; i++) {
|
|
183
|
+
const p = room.players[i];
|
|
184
|
+
if (!p) nodes.push('slot' + i + '=empty');
|
|
185
|
+
else if (p.bot) nodes.push('slot' + i + '=worker-' + i);
|
|
186
|
+
else if (i === mySeat) nodes.push('slot' + i + '=self');
|
|
187
|
+
else nodes.push('slot' + i + '=' + asciiName(p.name, 'peer' + i));
|
|
188
|
+
}
|
|
189
|
+
notify('nodes=[' + nodes.join(' ') + ']');
|
|
190
|
+
if (room.humans < room.seats) notify('waiting for peers ... (s=fill workers, q=leave)');
|
|
191
|
+
else notify('lobby ready, starting ...');
|
|
192
|
+
askWaiting();
|
|
94
193
|
return;
|
|
95
194
|
}
|
|
195
|
+
|
|
196
|
+
io.clear();
|
|
197
|
+
io.say('');
|
|
198
|
+
io.say(colors.bold(' 房间 ' + addr) + ' ' + colors.dim(room.gameName + ' · ' + room.seats + ' 人'));
|
|
199
|
+
io.say(' ' + '\u2500'.repeat(52));
|
|
200
|
+
for (let i = 0; i < room.seats; i++) {
|
|
201
|
+
const p = room.players[i];
|
|
202
|
+
let who;
|
|
203
|
+
if (!p) who = colors.dim('空 · 等待加入');
|
|
204
|
+
else if (p.bot) who = colors.dim(p.name + '(电脑)');
|
|
205
|
+
else if (i === mySeat) who = colors.green(p.name + '(你)');
|
|
206
|
+
else who = p.name;
|
|
207
|
+
io.say(' ' + (i + 1) + ' 号位 ' + who);
|
|
208
|
+
}
|
|
209
|
+
io.say(' ' + '\u2500'.repeat(52));
|
|
210
|
+
if (room.humans < room.seats) {
|
|
211
|
+
io.say(colors.dim(' 等待玩家 ' + room.humans + '/' + room.seats +
|
|
212
|
+
' 朋友加入: npx dou-cl --join ' + addr));
|
|
213
|
+
} else {
|
|
214
|
+
io.say(colors.dim(' 人齐了,马上开始…'));
|
|
215
|
+
}
|
|
216
|
+
askWaiting();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ---- 牌局 ---- */
|
|
220
|
+
|
|
221
|
+
function promptTurn(view) {
|
|
222
|
+
if (ui.skinActive()) {
|
|
223
|
+
io.ask(ui.skinPrompt(), function (line) { onTurnInput(line, view); });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const prompt = view.phase === 'bid'
|
|
227
|
+
? '\n叫分 (0=不叫 1/2/3=叫分, ?=帮助) > '
|
|
228
|
+
: '\n出牌 (例: 34567 或 5 5 5 6 / p 不要 / h 提示 / ? 帮助) > ';
|
|
229
|
+
io.ask(prompt, function (line) { onTurnInput(line, view); });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 轮到我:本地命令优先,其余当作出牌 */
|
|
233
|
+
function onTurnInput(line, view) {
|
|
234
|
+
if (!line) { promptTurn(view); return; }
|
|
235
|
+
if (isQuit(line)) { quit(); return; }
|
|
236
|
+
if (localCommand(line, view)) { promptTurn(view); return; }
|
|
237
|
+
playInput(line, view);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 不是我的回合:本地命令照样能用,想打牌就告诉他还没轮到你 */
|
|
241
|
+
function askIdle(view) {
|
|
242
|
+
const prompt = ui.skinActive() ? ui.skinPrompt()
|
|
243
|
+
: '\n' + colors.dim('[q] 退出 本地命令随时可用: reveal / redraw / skin <名> / ?') + ' > ';
|
|
244
|
+
io.ask(prompt, function (line) {
|
|
245
|
+
if (isQuit(line)) { quit(); return; }
|
|
246
|
+
if (localCommand(line, view)) { askIdle(view); return; }
|
|
247
|
+
if ((line || '').trim()) {
|
|
248
|
+
const hint = ui.skinActive()
|
|
249
|
+
? 'not your turn yet (local: reveal / redraw / skin <name> / help)'
|
|
250
|
+
: '还没轮到你。本地命令随时可用: reveal / redraw / skin <名> / ?';
|
|
251
|
+
notify(hint, hint);
|
|
252
|
+
}
|
|
253
|
+
askIdle(view);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function playInput(line, view) {
|
|
258
|
+
const low = line.toLowerCase();
|
|
259
|
+
const skin = ui.skinActive();
|
|
260
|
+
|
|
96
261
|
if (view.phase === 'bid') {
|
|
97
262
|
const v = parseInt(line, 10);
|
|
98
|
-
if (isNaN(v) || v < 0 || v > 3) {
|
|
263
|
+
if (isNaN(v) || v < 0 || v > 3) {
|
|
264
|
+
complain('叫分只能是 0/1/2/3', 'bid must be 0 / 1 / 2 / 3');
|
|
265
|
+
promptTurn(view);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
99
268
|
sendMsg({ t: 'act', action: { type: 'bid', score: v } });
|
|
100
269
|
return;
|
|
101
270
|
}
|
|
102
|
-
if (low === 'p' || low === 'pass' || line === '不要' || line === '过') {
|
|
103
|
-
if (!view.canPass) {
|
|
271
|
+
if (low === 'p' || low === 'pass' || line === '不要' || line === '过' || line === '不出') {
|
|
272
|
+
if (!view.canPass) {
|
|
273
|
+
complain('你是先手,必须出牌', 'you lead this round, must play');
|
|
274
|
+
promptTurn(view);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
104
277
|
sendMsg({ t: 'act', action: { type: 'pass' } });
|
|
105
278
|
return;
|
|
106
279
|
}
|
|
107
280
|
if (low === 'h' || low === 'hint' || line === '提示') {
|
|
108
281
|
const ids = hintIds(view, mySeat);
|
|
109
282
|
if (!ids || !ids.length) {
|
|
110
|
-
complain('没有能管上的牌,只能不要(pass)');
|
|
283
|
+
complain('没有能管上的牌,只能不要(pass)', 'nothing beats it, must pass');
|
|
111
284
|
} else {
|
|
112
285
|
const cards = view.hand.filter(function (c) { return ids.indexOf(c.id) >= 0; });
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
286
|
+
const co = comboMod.analyze(cards, rulesMod.get(view.key));
|
|
287
|
+
if (skin) {
|
|
288
|
+
const txt = 'hint ' + cards.map(function (c) { return skinsMod.packet.cardCode(c); }).join(' ') +
|
|
289
|
+
' [' + (co ? co.type : '') + ']';
|
|
290
|
+
io.block(ui.skinMessage(txt));
|
|
291
|
+
} else {
|
|
292
|
+
io.say('hint: ' + ui.handText(cards) + ' [' + (co ? comboMod.typeName(co.type) : '') + ']');
|
|
293
|
+
}
|
|
117
294
|
}
|
|
118
295
|
promptTurn(view);
|
|
119
296
|
return;
|
|
120
297
|
}
|
|
121
298
|
|
|
122
299
|
const parsed = ui.parseCardInput(line, view.hand);
|
|
123
|
-
if (!parsed.ok) { complain(parsed.err); promptTurn(view); return; }
|
|
300
|
+
if (!parsed.ok) { complain(parsed.err, parsed.en); promptTurn(view); return; }
|
|
124
301
|
sendMsg({ t: 'act', action: { type: 'play', ids: parsed.ids } });
|
|
125
302
|
}
|
|
126
303
|
|
|
127
|
-
function printHelp(key) {
|
|
128
|
-
const r = rulesMod.get(key);
|
|
129
|
-
const lines = [r.name + ' ' + r.tagline].concat(r.help);
|
|
130
|
-
lines.push('');
|
|
131
|
-
lines.push('出牌:34567 或 3 4 5 6 7 ;带花色 5s 5h 5c');
|
|
132
|
-
lines.push('p=不要 h=提示 ?=帮助 quit=离开');
|
|
133
|
-
if (ui.skinActive()) {
|
|
134
|
-
const s = ui.skinHelp(lines);
|
|
135
|
-
if (s !== null) { process.stdout.write(s); return; }
|
|
136
|
-
}
|
|
137
|
-
io.say('');
|
|
138
|
-
lines.forEach(function (l) { io.say(' ' + l); });
|
|
139
|
-
io.say('');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
304
|
function renderState(view) {
|
|
143
305
|
lastView = view;
|
|
144
306
|
const skin = ui.skinActive();
|
|
145
307
|
const skinned = ui.renderSkin(view, mySeat, {});
|
|
146
|
-
if (skinned !== null)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
308
|
+
if (skinned !== null) io.block(skinned);
|
|
309
|
+
else { io.clear(); process.stdout.write(ui.renderTable(view, mySeat, {}) + '\n'); }
|
|
310
|
+
|
|
311
|
+
if (view.over) {
|
|
312
|
+
const txt = (view.result && view.result.text) || '';
|
|
313
|
+
if (skin) notify('round done next round soon (q=leave)', 'round done next round soon (q=leave)');
|
|
314
|
+
else io.say(colors.dim(' 本局结束,马上开始下一局…(q 退出)'));
|
|
315
|
+
askIdle(view);
|
|
316
|
+
return;
|
|
151
317
|
}
|
|
152
|
-
if (view.over) { askAgain(); return; }
|
|
153
318
|
if (view.turn === mySeat && view.phase !== 'over') {
|
|
154
319
|
promptTurn(view);
|
|
155
320
|
} else {
|
|
156
|
-
io.handler = null;
|
|
157
321
|
const who = view.seats[view.turn] ? view.seats[view.turn].name : '?';
|
|
158
|
-
if (skin)
|
|
322
|
+
if (skin) notify('waiting for ' + asciiName(who, 'peer') + ' ...',
|
|
323
|
+
'waiting for ' + asciiName(who, 'peer') + ' ...');
|
|
159
324
|
else process.stdout.write(colors.dim(' 等待 ' + who + (view.phase === 'bid' ? ' 叫分…' : ' 出牌…')) + '\n');
|
|
325
|
+
askIdle(view);
|
|
160
326
|
}
|
|
161
327
|
}
|
|
162
328
|
|
|
163
|
-
|
|
164
|
-
io.ask(ui.skinActive() ? ui.skinPrompt() : '\n再来一局?(y/n) > ', function (line) {
|
|
165
|
-
sendMsg({ t: 'again', yes: /^y/i.test(line.trim()) });
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function renderLobby(room) {
|
|
170
|
-
if (ui.skinActive()) {
|
|
171
|
-
notify('pool=' + room.code + ' svc=deck-sync game=' + room.game + ' seats=' + room.seats);
|
|
172
|
-
const nodes = [];
|
|
173
|
-
for (let i = 0; i < room.seats; i++) {
|
|
174
|
-
const p = room.players[i];
|
|
175
|
-
if (!p) nodes.push('slot' + i + '=empty');
|
|
176
|
-
else if (p.bot) nodes.push('slot' + i + '=worker-' + i);
|
|
177
|
-
else nodes.push('slot' + i + '=' + asciiName(p.name, i === room.hostSeat ? 'self' : ('peer' + i)));
|
|
178
|
-
}
|
|
179
|
-
notify('nodes=[' + nodes.join(' ') + ']');
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
io.clear();
|
|
183
|
-
io.say('');
|
|
184
|
-
io.say(colors.bold(' 房间 ' + room.code) + ' ' + colors.dim(room.gameName + ' · ' + room.seats + ' 人'));
|
|
185
|
-
io.say(' ' + '\u2500'.repeat(50));
|
|
186
|
-
for (let i = 0; i < room.seats; i++) {
|
|
187
|
-
const p = room.players[i];
|
|
188
|
-
io.say(' ' + (i + 1) + '号位 ' + (p ? (p.bot ? colors.dim(p.name + '(电脑)') : p.name) : colors.dim('空')));
|
|
189
|
-
}
|
|
190
|
-
io.say(' ' + '\u2500'.repeat(50));
|
|
191
|
-
io.say(colors.dim(' 把房间号发给朋友,或让他们执行: npx dou-cl --join <地址> --code ' + room.code));
|
|
192
|
-
}
|
|
329
|
+
/* ---- 连接 ---- */
|
|
193
330
|
|
|
194
331
|
ws.on('open', function () {
|
|
195
|
-
if (!ui.skinActive()) { io.clear(); io.say(colors.green('
|
|
196
|
-
|
|
197
|
-
else if (opts.mode === 'join') sendMsg({ t: 'join', code: opts.code, name: opts.name });
|
|
198
|
-
else sendMsg({ t: 'create', game: opts.game, name: opts.name, public: false });
|
|
332
|
+
if (!ui.skinActive()) { io.clear(); io.say(colors.green('已连上 ' + addr + ',正在进入房间…')); }
|
|
333
|
+
sendMsg({ t: 'join', name: opts.name });
|
|
199
334
|
});
|
|
200
335
|
|
|
201
336
|
ws.on('message', function (raw) {
|
|
202
337
|
let m;
|
|
203
338
|
try { m = JSON.parse(String(raw)); } catch (e) { return; }
|
|
204
339
|
|
|
205
|
-
if (m.t === 'hello') {
|
|
340
|
+
if (m.t === 'hello') { gameKey = m.game; return; }
|
|
206
341
|
|
|
207
|
-
if (m.t === '
|
|
342
|
+
if (m.t === 'seated') {
|
|
208
343
|
mySeat = m.seat;
|
|
209
|
-
notify('
|
|
344
|
+
if (ui.skinActive()) notify('seated room=' + addr + ' slot=' + m.seat + ' game=' + gameKey);
|
|
210
345
|
return;
|
|
211
346
|
}
|
|
212
347
|
|
|
213
|
-
if (m.t === '
|
|
214
|
-
if (lastView &&
|
|
348
|
+
if (m.t === 'lobby') {
|
|
349
|
+
if (lastView && lastView.over) lastView = null;
|
|
350
|
+
if (m.room.started) return; // 已经开局了,等 state
|
|
215
351
|
renderLobby(m.room);
|
|
216
|
-
const isHost = mySeat >= 0 && m.room.hostSeat === mySeat;
|
|
217
|
-
if (isHost) {
|
|
218
|
-
io.ask(ui.skinActive() ? ui.skinPrompt()
|
|
219
|
-
: '\n' + colors.bold('按 Enter 开始游戏(不足的位置由电脑补上)> '),
|
|
220
|
-
function (line) {
|
|
221
|
-
const low = line.trim().toLowerCase();
|
|
222
|
-
if (low === 'q' || low === 'quit') { sendMsg({ t: 'leave' }); ws.close(); return; }
|
|
223
|
-
sendMsg({ t: 'start' });
|
|
224
|
-
});
|
|
225
|
-
} else {
|
|
226
|
-
io.handler = null;
|
|
227
|
-
if (ui.skinActive()) io.block(ui.skinMessage('waiting for host ...'));
|
|
228
|
-
else io.say(colors.dim(' 等房主开始…(输入 quit 离开)'));
|
|
229
|
-
}
|
|
230
352
|
return;
|
|
231
353
|
}
|
|
232
354
|
|
|
233
355
|
if (m.t === 'state') { renderState(m.view); return; }
|
|
234
|
-
if (m.t === 'notice') { notify(m.msg); return; }
|
|
356
|
+
if (m.t === 'notice') { notify(m.msg, m.en); return; }
|
|
357
|
+
|
|
235
358
|
if (m.t === 'chat') {
|
|
236
359
|
if (ui.skinActive()) notify(m.who + ': ' + m.msg);
|
|
237
360
|
else io.say(colors.cyan('[' + m.who + '] ') + m.msg);
|
|
238
361
|
return;
|
|
239
362
|
}
|
|
363
|
+
|
|
364
|
+
if (m.t === 'full') {
|
|
365
|
+
complain(m.msg, m.en);
|
|
366
|
+
if (!ui.skinActive()) {
|
|
367
|
+
io.say(colors.dim(' 换一个端口,或稍后再来(房间空了就能进)。'));
|
|
368
|
+
}
|
|
369
|
+
setTimeout(function () { try { ws.close(); } catch (e) { /* ignore */ } }, 150);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
240
373
|
if (m.t === 'error') {
|
|
241
|
-
complain(m.msg
|
|
242
|
-
if (mySeat < 0) ws.close();
|
|
243
|
-
else if (lastView) promptTurn(lastView);
|
|
374
|
+
complain(m.msg, m.en);
|
|
375
|
+
if (mySeat < 0) setTimeout(function () { try { ws.close(); } catch (e) { /* ignore */ } }, 150);
|
|
376
|
+
else if (lastView && !lastView.over) promptTurn(lastView);
|
|
244
377
|
return;
|
|
245
378
|
}
|
|
246
379
|
});
|
|
247
380
|
|
|
248
381
|
ws.on('close', function () {
|
|
382
|
+
dropSigint();
|
|
249
383
|
if (!ui.skinActive()) io.say(colors.dim('与服务器的连接已断开。'));
|
|
250
384
|
io.close();
|
|
251
385
|
});
|
|
252
386
|
|
|
253
387
|
ws.on('error', function (e) {
|
|
388
|
+
dropSigint();
|
|
254
389
|
if (!ui.skinActive()) {
|
|
255
390
|
io.say(colors.red('连接失败: ' + e.message));
|
|
256
|
-
io.say(colors.dim('
|
|
391
|
+
io.say(colors.dim('确认地址/端口正确,且那边已经开了房间(npx dou-cl --serve --port ' +
|
|
392
|
+
(addr.split(':')[1] || '8080') + ')。'));
|
|
393
|
+
} else {
|
|
394
|
+
io.block(ui.skinMessage('! connect failed: ' + e.message));
|
|
257
395
|
}
|
|
258
396
|
io.close();
|
|
259
397
|
});
|
|
260
398
|
}
|
|
261
399
|
|
|
262
|
-
module.exports = { runClient: runClient };
|
|
400
|
+
module.exports = { runClient: runClient, toUrl: toUrl };
|