dou-cl 1.0.1 → 1.1.0

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 CHANGED
@@ -238,9 +238,9 @@ npx dou-cl
238
238
  npx dou-cl -g doudizhu # 跳过菜单,直接开一局(最隐蔽)
239
239
  npx dou-cl -g fivek # 510K
240
240
  npx dou-cl --skin hex # 换个伪装皮肤
241
- npx dou-cl --serve --port 8080 # 当服务器,别人连你
242
- npx dou-cl --join 192.168.1.10:8080 --code ABCD
243
- npx dou-cl --match doudizhu --server 1.2.3.4:8080
241
+ npx dou-cl --serve --port 8080 -g doudizhu # 本机开一个房间(服务器不占座位)
242
+ npx dou-cl --join 192.168.1.10:8080 # 连进这个房间(地址本身就是房间)
243
+ npx dou-cl --serve --ports 10000-12000 # 一次开一片房间,每个端口一个
244
244
  ```
245
245
 
246
246
  ### ⚠️ 刚发完那几分钟,npx 会拉不到
package/README.md CHANGED
@@ -11,16 +11,47 @@
11
11
  <p align="center"><b>一个伪装成开发控制台的命令行牌桌。</b></p>
12
12
 
13
13
  跑起来就是一屏正常的开发输出:服务端日志、`xxd` 转储、JSON 帧、git diff。
14
- 里面是**斗地主 / 跑得快 / 510K**,牌局信息一条不删,只是换了层壳。
14
+ 里面其实是**斗地主 / 跑得快 / 510K**,牌局信息一条不删,只是换了层壳。
15
15
  工位上扫一眼屏幕的人,只会觉得你在看构建日志。
16
16
 
17
17
  ```bash
18
- npx dou-cl # 进菜单
19
- npx dou-cl -g doudizhu # 跳过菜单直接开一局
18
+ npx dou-cl --join 43.159.48.211:10001 # 进公共房间,和真人打
19
+ npx dou-cl # 单机,电脑陪打
20
+ npx dou-cl -g doudizhu # 跳过菜单直接开局(最隐蔽)
20
21
  ```
21
22
 
22
23
  ---
23
24
 
25
+ ## 公共房间
26
+
27
+ 有人开了一台公共服务器:**`43.159.48.211`,端口 `10000`–`12000`,一共 2001 个房间。**
28
+
29
+ 每个端口是一个独立房间,**随便挑一个进去就行** —— 或者可以和朋友约定一个端口。
30
+
31
+ ```bash
32
+ npx dou-cl --join 43.159.48.211:10001
33
+ ```
34
+
35
+ - 人不够会一直等着,**人齐了自动开局**;等的时候敲 `s` 让电脑先补上,`q` 退出
36
+ - 人满了会直接告诉你(`room full`),**换个端口**再试即可
37
+ - 只想自己练手就 `npx dou-cl`,单机三位电脑陪打
38
+
39
+ ---
40
+
41
+ ## 局域网联机
42
+
43
+ **一个 `ip:port` 就是一个房间**,服务器自己不占座位,只是托管。
44
+
45
+ ```bash
46
+ npx dou-cl --serve --port 8080 -g doudizhu # 本机开一个房间
47
+ npx dou-cl --join 192.168.1.10:8080 # 同事连进来,地址就是房间号
48
+ npx dou-cl --serve --ports 10000-12000 # 一次开一片,每个端口一个独立房间
49
+ ```
50
+
51
+ 换皮肤、`reveal` 这类纯本地的操作**任何时刻都能用**,不用等你的回合。
52
+
53
+ ---
54
+
24
55
  ## 四种伪装
25
56
 
26
57
  **`log`(默认)** —— 服务端日志流。看 `hand[...]` / `peers=[...]` / `table=`。
@@ -75,7 +106,7 @@ npx dou-cl --skins # 列出全部
75
106
  | 操作 | 写法 |
76
107
  | --- | --- |
77
108
  | 出牌 | 直接敲点数:`34567` 或 `3 4 5 6 7`;指定花色 `5s 5h` |
78
- | 不要 / 提示 / 帮助 / 退出 | `p` / `h` / `?` / `quit` |
109
+ | 不要 / 提示 / 帮助 / 退出 | `p` / `h` / `?` / `q` |
79
110
  | 看真身 / 换皮肤 / 重画 | `reveal` / `skin <名>` / `redraw` |
80
111
 
81
112
  同花顺、五十K **不用管花色**,直接敲 `56789` / `50K`,程序自动挑同花色那组。
@@ -90,14 +121,6 @@ npx dou-cl --skins # 列出全部
90
121
  | **跑得快** | 3 | 一副牌去大小王,持 ♠3 者先出且首手必带 ♠3 |
91
122
  | **510K** | 4 | 两副牌,5/10/K 计分;五十K 是通吃牌型 |
92
123
 
93
- 都可以联网:一个人 `--serve` 开服务器,其他人 `--join <地址> --code <房间号>` 进来,
94
- 或 `--match <玩法>` 快速匹配,人数不够由电脑补齐。
95
-
96
- ```bash
97
- npx dou-cl --serve --port 8080
98
- npx dou-cl --join 192.168.1.10:8080 --code ABCD
99
- ```
100
-
101
124
  ---
102
125
 
103
126
  ## License
package/bin/dou-cl.js CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  /*
5
5
  * dou-cl —— 命令行斗地主 / 跑得快 / 510K
6
- * 单机可玩,也支持联网(自建服务器 / 局域网 / 快速匹配)
6
+ * 单机可玩;联网时「一个 ip:port 就是一个房间」。
7
7
  */
8
8
 
9
9
  const path = require('path');
@@ -27,12 +27,12 @@ function usage() {
27
27
  console.log(' npx dou-cl -g paodekuai 跑得快');
28
28
  console.log(' npx dou-cl -g fivek 510K');
29
29
  console.log('');
30
- console.log(colors.bold(' 联网'));
31
- console.log(' npx dou-cl --serve 在本机 8080 端口开服务器');
32
- console.log(' npx dou-cl --serve --port 9000 指定端口');
33
- console.log(' npx dou-cl --create doudizhu 连服务器并开一个房间');
34
- console.log(' npx dou-cl --join 1.2.3.4:8080 --code ABCD');
35
- console.log(' npx dou-cl --match doudizhu --server 1.2.3.4:8080 快速匹配');
30
+ console.log(colors.bold(' 联网') + colors.dim('(一个 ip:port 就是一个房间)'));
31
+ console.log(' npx dou-cl --serve --port 8080 在 8080 开一个房间(默认斗地主)');
32
+ console.log(' npx dou-cl --serve --port 8080 -g fivek 指定房间玩法');
33
+ console.log(' npx dou-cl --serve --ports 10000-12000 一次开一片房间,每个端口一个');
34
+ console.log(' npx dou-cl --join 192.168.1.10:8080 连进这个房间');
35
+ console.log(colors.dim(' 服务器自己不占座位;人满了直接回绝,人不够就等着(s 让电脑补齐,q 退出)'));
36
36
  console.log('');
37
37
  console.log(colors.bold(' 摸鱼模式') + colors.dim('(默认开启,牌桌伪装成看起来正常的开发输出)'));
38
38
  console.log(' npx dou-cl --skin hex 换皮肤: log(默认) / hex / json / diff');
@@ -42,6 +42,7 @@ function usage() {
42
42
  console.log('');
43
43
  console.log(colors.bold(' 其他'));
44
44
  console.log(' --name <昵称> 联网时显示的名字');
45
+ console.log(' --host <地址> 服务器监听地址(默认 0.0.0.0)');
45
46
  console.log(' --speed <毫秒> 单机电脑出牌间隔(默认 700)');
46
47
  console.log(' --ascii 花色用 S/H/C/D 字母显示(老终端兼容)');
47
48
  console.log(' -h, --help 显示帮助');
@@ -59,14 +60,36 @@ function usage() {
59
60
  console.log('');
60
61
  }
61
62
 
63
+ /** "8080" / "8080,8081" / "10000-12000" → [8080] / [8080,8081] / [10000..12000] */
64
+ function parsePorts(s) {
65
+ const out = [];
66
+ String(s || '').split(',').forEach(function (part) {
67
+ const p = part.trim();
68
+ if (!p) return;
69
+ const mm = /^(\d+)\s*-\s*(\d+)$/.exec(p);
70
+ if (mm) {
71
+ const a = Number(mm[1]);
72
+ const b = Number(mm[2]);
73
+ for (let i = Math.min(a, b); i <= Math.max(a, b); i++) {
74
+ if (i > 0 && i < 65536) out.push(i);
75
+ }
76
+ } else {
77
+ const n = Number(p);
78
+ if (n > 0 && n < 65536) out.push(n);
79
+ }
80
+ });
81
+ return out;
82
+ }
83
+
62
84
  function parseArgs(argv) {
63
85
  const o = {
64
86
  mode: 'menu',
65
87
  game: null,
66
88
  server: process.env.DOUCL_SERVER || '127.0.0.1:8080',
67
- code: null,
68
89
  name: process.env.DOUCL_NAME || null,
69
90
  port: Number(process.env.DOUCL_PORT) || 8080,
91
+ ports: null,
92
+ host: process.env.DOUCL_HOST || null,
70
93
  speed: 700,
71
94
  ascii: false,
72
95
  skin: process.env.DOUCL_SKIN || require('../src/skins').DEFAULT_SKIN,
@@ -85,14 +108,13 @@ function parseArgs(argv) {
85
108
  case '-v': case '--version': o.version = true; break;
86
109
  case '--serve': case '--server-mode': o.mode = 'serve'; break;
87
110
  case '--join': o.mode = 'join'; o.server = next(); break;
88
- case '--match': o.mode = 'match'; o.game = next(); break;
89
- case '--create': o.mode = 'create'; o.game = next(); break;
111
+ case '--server': o.mode = 'join'; o.server = next(); break;
90
112
  case '--local': o.mode = 'local'; o.game = next(); break;
91
- case '-g': case '--game': o.mode = 'local'; o.game = next(); break;
92
- case '--server': o.server = next(); break;
93
- case '--code': o.code = (next() || '').toUpperCase(); break;
94
- case '--name': o.name = next(); break;
113
+ case '-g': case '--game': o.mode = o.mode === 'serve' ? 'serve' : 'local'; o.game = next(); break;
114
+ case '--ports': case '--port-range': o.ports = parsePorts(next()); break;
95
115
  case '--port': o.port = Number(next()) || 8080; break;
116
+ case '--host': o.host = next(); break;
117
+ case '--name': o.name = next(); break;
96
118
  case '--speed': o.speed = Number(next()) || 700; break;
97
119
  case '--ascii': o.ascii = true; break;
98
120
  case '--no-color': process.env.NO_COLOR = '1'; break;
@@ -118,6 +140,13 @@ function parseArgs(argv) {
118
140
  case '-s':
119
141
  o.skin = 'log';
120
142
  break;
143
+ // 旧协议里的房间号 / 匹配 / 开房已经取消:地址本身就是房间
144
+ case '--code': case '--match': case '--create': {
145
+ console.error(colors.yellow('提示:' + a + ' 已经不需要了 —— 现在一个地址就是一个房间。'));
146
+ console.error(colors.dim(' 直接: npx dou-cl --serve --port 8080 然后别人 npx dou-cl --join <IP>:8080'));
147
+ if ((a === '--match' || a === '--create') && args[i + 1] && args[i + 1][0] !== '-') i++;
148
+ break;
149
+ }
121
150
  default:
122
151
  if (a && a[0] !== '-') {
123
152
  if (a === 'exercise' || a === 'drill') { o.mode = 'exercise'; o.exercise = 'menu'; }
@@ -168,32 +197,23 @@ async function main() {
168
197
 
169
198
  if (opts.mode === 'serve') {
170
199
  const server = require('../src/net/server');
171
- const s = server.createServer({ port: opts.port });
172
- console.log(colors.bold(' dou-cl 服务器运行中') + colors.dim(' 端口 ' + opts.port));
200
+ const game = opts.game || 'doudizhu';
201
+ const ports = (opts.ports && opts.ports.length) ? opts.ports : [opts.port];
202
+ const s = server.createServers({ game: game, ports: ports, host: opts.host || '0.0.0.0' });
203
+ console.log(colors.dim(' 服务端不占座位 —— 玩家执行 npx dou-cl --join <本机IP>:<端口> 进来'));
173
204
  console.log(colors.dim(' Ctrl+C 停止'));
205
+ void s;
174
206
  return;
175
207
  }
176
208
 
177
- if (opts.mode === 'create' || opts.mode === 'join' || opts.mode === 'match') {
209
+ if (opts.mode === 'join') {
178
210
  const client = require('../src/net/client');
179
211
  const io = new ui.IO();
180
- if (opts.mode === 'join' && !opts.code) {
181
- const code = await app.ask(io, ' 房间号(4 位)> ');
182
- opts.code = code.toUpperCase();
183
- }
184
- if (opts.mode === 'create' && !opts.game) opts.game = 'doudizhu';
185
- if (opts.mode === 'match' && !opts.game) opts.game = 'doudizhu';
186
212
  if (!opts.name) {
187
213
  const n = await app.ask(io, ' 你的昵称 > ');
188
214
  opts.name = n || '玩家';
189
215
  }
190
- await client.runClient(io, {
191
- mode: opts.mode,
192
- game: opts.game,
193
- code: opts.code,
194
- name: opts.name,
195
- server: opts.server,
196
- }, opts);
216
+ await client.runClient(io, { server: opts.server, name: opts.name }, opts);
197
217
  return;
198
218
  }
199
219
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dou-cl",
3
- "version": "1.0.1",
4
- "description": "伪装成开发控制台的命令行牌桌 —— 斗地主 / 跑得快 / 510K,牌局藏在终端里",
3
+ "version": "1.1.0",
4
+ "description": "伪装成开发控制台的命令行牌桌 —— 斗地主 / 跑得快 / 510K,牌局藏在日志流、xxd、JSON 或 git diff 里;支持局域网联机,也有公共房间可直接和真人开局",
5
5
  "keywords": [
6
6
  "doudizhu",
7
7
  "斗地主",
@@ -12,7 +12,9 @@
12
12
  "card-game",
13
13
  "terminal",
14
14
  "tui",
15
- "stealth"
15
+ "stealth",
16
+ "multiplayer",
17
+ "lan"
16
18
  ],
17
19
  "license": "MIT",
18
20
  "author": "iguangyu",
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' ) { printSkinHelp(); return step(); }
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' || c === '5' || c === '6') {
281
- onlineFlow(io, String(Number(c) - 3), opts).catch(function (e) {
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 === '7') { startServerFlow(io, opts.port); return; }
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
- async function onlineFlow(io, choice, opts) {
296
+ /** 联机 · 加入房间:问昵称 + 地址,地址本身就是房间 */
297
+ async function joinFlow(io, opts) {
300
298
  io.clear();
301
299
  io.say('');
302
- const name = await ask(io, ' 你的昵称 > ') || '玩家';
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.say(' 选择玩法: 1 斗地主 2 跑得快 3 510K');
327
- const g = await ask(io, ' > ');
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(), { mode: 'match', game: key, name: name, server: srv }, opts);
306
+ await client.runClient(new ui.IO(), { server: srv, name: name }, opts);
333
307
  }
334
308
 
335
- function startServerFlow(io, port) {
309
+ /** 联机 · 本机开一个房间(服务器自己不占座位,别人用 --join 连进来) */
310
+ async function startServerFlow(io, opts) {
336
311
  io.clear();
337
312
  io.say('');
338
- io.say(' 启动服务器…(Ctrl+C 停止)');
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.createServer({ port: port || 8080 });
341
- io.say(colors.dim(' 本机 IP 可以用 ipconfig 查,别人执行: npx dou-cl --join <你的IP>:' + (port || 8080)));
342
- io.say(colors.dim(' 也可以让别人快速匹配: npx dou-cl --match doudizhu --server <你的IP>:' + (port || 8080)));
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