dou-cl 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/PUBLISH.md +322 -0
- package/README.md +229 -2
- package/bin/dou-cl.js +215 -0
- package/package.json +61 -4
- package/src/app.js +356 -0
- package/src/bot.js +263 -0
- package/src/color.js +26 -0
- package/src/core/cards.js +152 -0
- package/src/core/combo.js +569 -0
- package/src/core/rules.js +92 -0
- package/src/exercise.js +682 -0
- package/src/games/base.js +280 -0
- package/src/games/doudizhu.js +146 -0
- package/src/games/fivek.js +77 -0
- package/src/games/index.js +25 -0
- package/src/games/paodekuai.js +48 -0
- package/src/net/client.js +262 -0
- package/src/net/server.js +432 -0
- package/src/runner.js +61 -0
- package/src/skins/diff.js +147 -0
- package/src/skins/hex.js +174 -0
- package/src/skins/index.js +116 -0
- package/src/skins/json.js +151 -0
- package/src/skins/log.js +111 -0
- package/src/skins/packet.js +144 -0
- package/src/ui.js +395 -0
package/bin/dou-cl.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* dou-cl —— 命令行斗地主 / 跑得快 / 510K
|
|
6
|
+
* 单机可玩,也支持联网(自建服务器 / 局域网 / 快速匹配)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
11
|
+
|
|
12
|
+
const rulesMod = require('../src/core/rules');
|
|
13
|
+
const gamesMod = require('../src/games');
|
|
14
|
+
const ui = require('../src/ui');
|
|
15
|
+
const app = require('../src/app');
|
|
16
|
+
const skins = require('../src/skins');
|
|
17
|
+
|
|
18
|
+
const colors = ui.colors;
|
|
19
|
+
|
|
20
|
+
function usage() {
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log(colors.bold(' dou-cl v' + pkg.version) + colors.dim(' 命令行斗地主 / 跑得快 / 510K'));
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(colors.bold(' 用法'));
|
|
25
|
+
console.log(' npx dou-cl 进菜单(默认就是摸鱼模式)');
|
|
26
|
+
console.log(' npx dou-cl -g doudizhu 直接开一局斗地主(跳过菜单,最隐蔽)');
|
|
27
|
+
console.log(' npx dou-cl -g paodekuai 跑得快');
|
|
28
|
+
console.log(' npx dou-cl -g fivek 510K');
|
|
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 快速匹配');
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(colors.bold(' 摸鱼模式') + colors.dim('(默认开启,牌桌伪装成看起来正常的开发输出)'));
|
|
38
|
+
console.log(' npx dou-cl --skin hex 换皮肤: log(默认) / hex / json / diff');
|
|
39
|
+
console.log(' npx dou-cl --skin term 关掉伪装,显示原生牌桌');
|
|
40
|
+
console.log(' npx dou-cl --skins 列出所有皮肤');
|
|
41
|
+
console.log(colors.dim(' 游戏里随时可以: reveal 看真身 / skin <名> 换皮肤 / ? 帮助'));
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(colors.bold(' 其他'));
|
|
44
|
+
console.log(' --name <昵称> 联网时显示的名字');
|
|
45
|
+
console.log(' --speed <毫秒> 单机电脑出牌间隔(默认 700)');
|
|
46
|
+
console.log(' --ascii 花色用 S/H/C/D 字母显示(老终端兼容)');
|
|
47
|
+
console.log(' -h, --help 显示帮助');
|
|
48
|
+
console.log(' -v, --version 显示版本');
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(colors.bold(' 牌面表示'));
|
|
51
|
+
console.log(' 3 4 5 6 7 8 9 0(=10) J Q K A 2 w=小王 W=大王');
|
|
52
|
+
console.log(' 花色:♠ ♥ ♣ ♦(--ascii 时为 S H C D)');
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(colors.bold(' 玩法'));
|
|
55
|
+
gamesMod.keys.forEach(function (k) {
|
|
56
|
+
const r = rulesMod.get(k);
|
|
57
|
+
console.log(' ' + r.name + colors.dim(' ' + r.tagline));
|
|
58
|
+
});
|
|
59
|
+
console.log('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseArgs(argv) {
|
|
63
|
+
const o = {
|
|
64
|
+
mode: 'menu',
|
|
65
|
+
game: null,
|
|
66
|
+
server: process.env.DOUCL_SERVER || '127.0.0.1:8080',
|
|
67
|
+
code: null,
|
|
68
|
+
name: process.env.DOUCL_NAME || null,
|
|
69
|
+
port: Number(process.env.DOUCL_PORT) || 8080,
|
|
70
|
+
speed: 700,
|
|
71
|
+
ascii: false,
|
|
72
|
+
skin: process.env.DOUCL_SKIN || require('../src/skins').DEFAULT_SKIN,
|
|
73
|
+
exercise: null,
|
|
74
|
+
listSkins: false,
|
|
75
|
+
help: false,
|
|
76
|
+
version: false,
|
|
77
|
+
};
|
|
78
|
+
const args = argv.slice(2);
|
|
79
|
+
const skinKeys = skins.list().map(function (s) { return s.key; });
|
|
80
|
+
for (let i = 0; i < args.length; i++) {
|
|
81
|
+
const a = args[i];
|
|
82
|
+
const next = function () { return args[++i]; };
|
|
83
|
+
switch (a) {
|
|
84
|
+
case '-h': case '--help': o.help = true; break;
|
|
85
|
+
case '-v': case '--version': o.version = true; break;
|
|
86
|
+
case '--serve': case '--server-mode': o.mode = 'serve'; break;
|
|
87
|
+
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;
|
|
90
|
+
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;
|
|
95
|
+
case '--port': o.port = Number(next()) || 8080; break;
|
|
96
|
+
case '--speed': o.speed = Number(next()) || 700; break;
|
|
97
|
+
case '--ascii': o.ascii = true; break;
|
|
98
|
+
case '--no-color': process.env.NO_COLOR = '1'; break;
|
|
99
|
+
case '--skins': case '--list-skins': o.listSkins = true; break;
|
|
100
|
+
case '--plain': case '--term': case '--no-stealth': o.skin = 'term'; break;
|
|
101
|
+
case '--skin': o.skin = next() || 'log'; break;
|
|
102
|
+
// 训练模式:刻意不写进 usage(),只有知道的人会用
|
|
103
|
+
case '-e': case '--exercise': case '--drill': {
|
|
104
|
+
o.mode = 'exercise';
|
|
105
|
+
o.exercise = 'menu';
|
|
106
|
+
const nxt = args[i + 1];
|
|
107
|
+
if (nxt && nxt[0] !== '-' &&
|
|
108
|
+
(require('../src/exercise').DRILLS.indexOf(nxt) >= 0 || nxt === 'mixed')) {
|
|
109
|
+
o.exercise = args[++i];
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
// 兼容旧写法:--stealth 现在就是默认行为,带上它只是可选地指定皮肤
|
|
114
|
+
case '--stealth': case '--moyu': case '--blend':
|
|
115
|
+
o.skin = 'log';
|
|
116
|
+
if (args[i + 1] && skinKeys.indexOf(args[i + 1]) >= 0) o.skin = args[++i];
|
|
117
|
+
break;
|
|
118
|
+
case '-s':
|
|
119
|
+
o.skin = 'log';
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
if (a && a[0] !== '-') {
|
|
123
|
+
if (a === 'exercise' || a === 'drill') { o.mode = 'exercise'; o.exercise = 'menu'; }
|
|
124
|
+
else if (gamesMod.keys.indexOf(a) >= 0) { o.mode = o.mode === 'menu' ? 'local' : o.mode; o.game = a; }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (o.game && gamesMod.keys.indexOf(o.game) < 0) {
|
|
129
|
+
console.error('未知玩法:' + o.game + '(可选:' + gamesMod.keys.join(', ') + ')');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (o.skin && o.skin !== 'term' && skinKeys.indexOf(o.skin) < 0) {
|
|
133
|
+
console.error('没有这个皮肤:' + o.skin + '(可选:term, ' + skinKeys.join(', ') + ')');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
if (o.speed < 0) o.speed = 0;
|
|
137
|
+
return o;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function main() {
|
|
141
|
+
const opts = parseArgs(process.argv);
|
|
142
|
+
ui.setAscii(opts.ascii);
|
|
143
|
+
|
|
144
|
+
if (opts.listSkins) {
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(colors.bold(' 伪装皮肤') + colors.dim(' (默认 log,游戏里敲 skin <名> 可以随时换)'));
|
|
147
|
+
skins.list().forEach(function (s) {
|
|
148
|
+
const mark = (s.key === skins.DEFAULT_SKIN) ? colors.green(' (默认)') : '';
|
|
149
|
+
console.log(' ' + colors.bold(s.key.padEnd(7)) + colors.dim(s.desc) + mark);
|
|
150
|
+
});
|
|
151
|
+
console.log(' ' + colors.bold('term'.padEnd(7)) + colors.dim('原生牌桌(关掉伪装,用 --skin term)'));
|
|
152
|
+
console.log('');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (opts.help) { usage(); return; }
|
|
157
|
+
if (opts.version) { console.log(pkg.version); return; }
|
|
158
|
+
|
|
159
|
+
ui.setSkin(opts.skin);
|
|
160
|
+
|
|
161
|
+
if (opts.mode === 'exercise') {
|
|
162
|
+
const exercise = require('../src/exercise');
|
|
163
|
+
const io = new ui.IO();
|
|
164
|
+
io.onClose = function () { process.exit(0); };
|
|
165
|
+
await exercise.run(io, opts, opts.exercise || 'menu');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (opts.mode === 'serve') {
|
|
170
|
+
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));
|
|
173
|
+
console.log(colors.dim(' Ctrl+C 停止'));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (opts.mode === 'create' || opts.mode === 'join' || opts.mode === 'match') {
|
|
178
|
+
const client = require('../src/net/client');
|
|
179
|
+
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
|
+
if (!opts.name) {
|
|
187
|
+
const n = await app.ask(io, ' 你的昵称 > ');
|
|
188
|
+
opts.name = n || '玩家';
|
|
189
|
+
}
|
|
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);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const io = new ui.IO();
|
|
201
|
+
io.onClose = function () { process.exit(0); };
|
|
202
|
+
|
|
203
|
+
if (opts.mode === 'local') {
|
|
204
|
+
await app.playLocal(io, opts.game, opts);
|
|
205
|
+
app.showMenu(io, opts);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
app.showMenu(io, opts);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
main().catch(function (e) {
|
|
213
|
+
console.error(colors.red('运行出错: ' + (e && e.stack || e)));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dou-cl",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "命令行斗地主 / 跑得快 / 510K —— 默认把牌桌伪装成普通开发输出(日志/hexdump/JSON/git diff),支持单机与联网对战",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"doudizhu",
|
|
7
|
+
"斗地主",
|
|
8
|
+
"跑得快",
|
|
9
|
+
"510k",
|
|
10
|
+
"cli",
|
|
11
|
+
"game",
|
|
12
|
+
"card-game",
|
|
13
|
+
"terminal",
|
|
14
|
+
"tui",
|
|
15
|
+
"stealth"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "iguangyu",
|
|
19
|
+
"homepage": "https://github.com/iguangyu/dou-cl#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/iguangyu/dou-cl.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/iguangyu/dou-cl/issues"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=16"
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"dou-cl": "bin/dou-cl.js",
|
|
32
|
+
"doucl": "bin/dou-cl.js"
|
|
33
|
+
},
|
|
34
|
+
"main": "src/app.js",
|
|
35
|
+
"files": [
|
|
36
|
+
"bin",
|
|
37
|
+
"src",
|
|
38
|
+
"README.md",
|
|
39
|
+
"PUBLISH.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"start": "node bin/dou-cl.js",
|
|
44
|
+
"serve": "node bin/dou-cl.js --serve",
|
|
45
|
+
"plain": "node bin/dou-cl.js --skin term",
|
|
46
|
+
"hex": "node bin/dou-cl.js --skin hex",
|
|
47
|
+
"test": "node test/sim.js && node test/smoke.js && node test/local-test.js && node test/skins-test.js && node test/exercise-test.js",
|
|
48
|
+
"sim": "node test/sim.js",
|
|
49
|
+
"smoke": "node test/smoke.js",
|
|
50
|
+
"local": "node test/local-test.js",
|
|
51
|
+
"skins": "node test/skins-test.js",
|
|
52
|
+
"drill": "node test/exercise-test.js",
|
|
53
|
+
"preview": "node test/make-preview.js",
|
|
54
|
+
"prepublishOnly": "npm test"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"ws": "^8.18.0"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"registry": "https://registry.npmjs.org/",
|
|
61
|
+
"access": "public"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const rulesMod = require('./core/rules');
|
|
4
|
+
const gamesMod = require('./games');
|
|
5
|
+
const bot = require('./bot');
|
|
6
|
+
const ui = require('./ui');
|
|
7
|
+
const runner = require('./runner');
|
|
8
|
+
const comboMod = require('./core/combo');
|
|
9
|
+
|
|
10
|
+
const colors = ui.colors;
|
|
11
|
+
const sleep = runner.sleep;
|
|
12
|
+
|
|
13
|
+
/* ------------------------------------------------------------------ */
|
|
14
|
+
/* 帮助 */
|
|
15
|
+
/* ------------------------------------------------------------------ */
|
|
16
|
+
|
|
17
|
+
function printHelp(key) {
|
|
18
|
+
const r = rulesMod.get(key);
|
|
19
|
+
const skinned = ui.skinHelp(r.help);
|
|
20
|
+
if (skinned !== null) { process.stdout.write(skinned); return; }
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log(colors.bold(' ' + r.name) + colors.dim(' ' + r.tagline));
|
|
23
|
+
r.help.forEach(function (h) { console.log(' ' + h); });
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log(colors.bold(' 操作'));
|
|
26
|
+
console.log(' 出牌:直接输入点数,例 3 4 5 6 7 或 34567');
|
|
27
|
+
console.log(' 要指定花色就写在点数后面,例 5s 5h 5c');
|
|
28
|
+
console.log(' 点数对照: 3 4 5 6 7 8 9 0(=10) J Q K A 2 w(小王) W(大王)');
|
|
29
|
+
console.log(' 命令: p / pass = 不要 h / hint = 提示 ? = 帮助 quit = 退出');
|
|
30
|
+
console.log('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 摸鱼模式下的“玩法说明”(同样是伪装的) */
|
|
34
|
+
function printStealthHelp() {
|
|
35
|
+
const lines = [];
|
|
36
|
+
lines.push('输出里每张牌是两个字符:点数 + 花色');
|
|
37
|
+
lines.push(' 点数 3 4 5 6 7 8 9 0(=10) J Q K A 2 w=小王 W=大王');
|
|
38
|
+
lines.push(' 花色 s=黑桃 h=红桃 c=梅花 d=方块');
|
|
39
|
+
lines.push('');
|
|
40
|
+
lines.push('hand[] = 你自己的手牌(就是这两个字符一组)');
|
|
41
|
+
lines.push('peers[] = 其他人还剩几张');
|
|
42
|
+
lines.push('table= = 场上最大的一手;管得上就出,管不上就 pass');
|
|
43
|
+
lines.push('turn= = 轮到谁,self 是你');
|
|
44
|
+
lines.push('');
|
|
45
|
+
lines.push('出牌 34567 或 3 4 5 6 7 带花色 5s 5h');
|
|
46
|
+
lines.push('pass | p hint | h redraw reveal 看真身 skin <名> quit');
|
|
47
|
+
const skinned = ui.skinHelp(lines);
|
|
48
|
+
process.stdout.write(skinned !== null ? skinned : lines.join('\n') + '\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function printSkinHelp() {
|
|
52
|
+
const list = ui.skinList();
|
|
53
|
+
const lines = ['可用皮肤:'];
|
|
54
|
+
lines.push(' term 原生牌桌(默认)');
|
|
55
|
+
list.forEach(function (s) { lines.push(' ' + s.key.padEnd(6) + ' ' + s.desc); });
|
|
56
|
+
const skinned = ui.skinHelp(lines);
|
|
57
|
+
process.stdout.write(skinned !== null ? skinned : lines.join('\n') + '\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ------------------------------------------------------------------ */
|
|
61
|
+
/* 渲染 */
|
|
62
|
+
/* ------------------------------------------------------------------ */
|
|
63
|
+
|
|
64
|
+
/** 本地对局里人类永远坐 0 号位 —— 渲染视角必须固定在他身上,
|
|
65
|
+
* 不能跟着“当前出牌方”走,否则电脑出牌时会把电脑的手牌当你的显示出来。 */
|
|
66
|
+
const HUMAN = 0;
|
|
67
|
+
|
|
68
|
+
function renderLocal(io, engine, state) {
|
|
69
|
+
const view = engine.view(state, HUMAN);
|
|
70
|
+
const skinned = ui.renderSkin(view, HUMAN, {});
|
|
71
|
+
if (skinned !== null) {
|
|
72
|
+
io.block(skinned);
|
|
73
|
+
} else {
|
|
74
|
+
io.clear();
|
|
75
|
+
process.stdout.write(ui.renderTable(view, HUMAN, {}) + '\n');
|
|
76
|
+
}
|
|
77
|
+
return view;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ------------------------------------------------------------------ */
|
|
81
|
+
/* 人类输入 */
|
|
82
|
+
/* ------------------------------------------------------------------ */
|
|
83
|
+
|
|
84
|
+
function askHuman(io, engine, state, seat, key) {
|
|
85
|
+
return new Promise(function (resolve) {
|
|
86
|
+
const skin = ui.skinActive();
|
|
87
|
+
|
|
88
|
+
const complain = function (msg) {
|
|
89
|
+
io.block(skin ? ui.skinMessage(msg) : colors.red(msg));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const step = function () {
|
|
93
|
+
const view = engine.view(state, seat);
|
|
94
|
+
let promptText;
|
|
95
|
+
if (skin) promptText = ui.skinPrompt();
|
|
96
|
+
else if (view.phase === 'bid') promptText = '\n叫分 (0=不叫 1/2/3=叫分, ?=帮助) > ';
|
|
97
|
+
else promptText = '\n出牌 > ';
|
|
98
|
+
|
|
99
|
+
io.ask(promptText, function (line) {
|
|
100
|
+
const low = line.toLowerCase();
|
|
101
|
+
if (!line) return step();
|
|
102
|
+
|
|
103
|
+
// ---- 摸鱼模式专用命令 ----
|
|
104
|
+
if (skin) {
|
|
105
|
+
if (low === 'reveal' || low === 'real') {
|
|
106
|
+
process.stdout.write(ui.renderTable(view, seat, {}) + '\n');
|
|
107
|
+
return step();
|
|
108
|
+
}
|
|
109
|
+
if (low === 'redraw' || low === 'rr') {
|
|
110
|
+
io.block(ui.renderSkin(view, seat, {}));
|
|
111
|
+
return step();
|
|
112
|
+
}
|
|
113
|
+
if (low.indexOf('skin') === 0) {
|
|
114
|
+
const k = low.split(/\s+/)[1];
|
|
115
|
+
if (!k) { printSkinHelp(); return step(); }
|
|
116
|
+
try {
|
|
117
|
+
ui.setSkin(k);
|
|
118
|
+
io.block(ui.skinMessage('skin -> ' + ui.skinKey()));
|
|
119
|
+
} catch (e) {
|
|
120
|
+
io.block(ui.skinMessage(e.message));
|
|
121
|
+
}
|
|
122
|
+
return step();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (low === '?' || low === 'help' || low === 'rules') {
|
|
127
|
+
if (skin) printStealthHelp(); else printHelp(key);
|
|
128
|
+
return step();
|
|
129
|
+
}
|
|
130
|
+
if (low === 'skins' || low === 'skin' ) { printSkinHelp(); return step(); }
|
|
131
|
+
if (low === 'quit' || low === 'exit') {
|
|
132
|
+
if (!skin) io.say(colors.dim('已退出本局。'));
|
|
133
|
+
io.close();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let action = null;
|
|
138
|
+
|
|
139
|
+
if (view.phase === 'bid') {
|
|
140
|
+
const v = parseInt(line, 10);
|
|
141
|
+
if (isNaN(v) || v < 0 || v > 3) { complain('叫分只能是 0 / 1 / 2 / 3'); return step(); }
|
|
142
|
+
action = { type: 'bid', score: v };
|
|
143
|
+
} else if (low === 'p' || low === 'pass' || line === '不要' || line === '过' || line === '不出') {
|
|
144
|
+
action = { type: 'pass' };
|
|
145
|
+
} else if (low === 'h' || low === 'hint' || line === '提示') {
|
|
146
|
+
const act = bot.chooseAction(engine, state, seat);
|
|
147
|
+
let msg;
|
|
148
|
+
if (act.type === 'pass') {
|
|
149
|
+
msg = '没有能管上的牌,只能不要(pass)';
|
|
150
|
+
} else {
|
|
151
|
+
const cards = view.hand.filter(function (c) { return act.ids.indexOf(c.id) >= 0; });
|
|
152
|
+
const co = comboMod.analyze(cards, engine.rules);
|
|
153
|
+
msg = 'hint: ' + ui.handText(cards) + ' [' + (co ? comboMod.typeName(co.type) : '') + ']';
|
|
154
|
+
}
|
|
155
|
+
io.block(skin ? ui.skinMessage(msg) : msg);
|
|
156
|
+
return step();
|
|
157
|
+
} else {
|
|
158
|
+
const parsed = ui.parseCardInput(line, view.hand);
|
|
159
|
+
if (!parsed.ok) { complain(parsed.err); return step(); }
|
|
160
|
+
action = { type: 'play', ids: parsed.ids };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const v = engine.validate(state, seat, action);
|
|
164
|
+
if (!v.ok) { complain(v.err); return step(); }
|
|
165
|
+
resolve(action);
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
step();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ------------------------------------------------------------------ */
|
|
173
|
+
/* 本地(单机)对局 */
|
|
174
|
+
/* ------------------------------------------------------------------ */
|
|
175
|
+
|
|
176
|
+
async function playLocal(io, key, opts) {
|
|
177
|
+
const rules = rulesMod.get(key);
|
|
178
|
+
const engine = gamesMod.create(key);
|
|
179
|
+
const names = ['你', '小美', '阿飞', '老王', '阿强'];
|
|
180
|
+
const players = [];
|
|
181
|
+
for (let i = 0; i < rules.seats; i++) {
|
|
182
|
+
players.push({ name: i === 0 ? '你' : names[i], bot: i !== 0 });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
ui.resetSkin();
|
|
186
|
+
|
|
187
|
+
const results = [];
|
|
188
|
+
let round = 1;
|
|
189
|
+
while (true) {
|
|
190
|
+
if (ui.skinActive()) {
|
|
191
|
+
io.block(ui.skinMessage('session_start pool=' + require('./skins').session().pool +
|
|
192
|
+
' svc=deck-sync game=' + key + ' seats=' + rules.seats + ' round=' + round));
|
|
193
|
+
} else {
|
|
194
|
+
io.clear();
|
|
195
|
+
io.say('');
|
|
196
|
+
io.say(colors.bold(' ' + rules.name + ' · 第 ' + round + ' 局'));
|
|
197
|
+
io.say(colors.dim(' ' + rules.tagline));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const decider = async function (state, seat) {
|
|
201
|
+
renderLocal(io, engine, state);
|
|
202
|
+
if (players[seat].bot) {
|
|
203
|
+
await sleep(opts.speed);
|
|
204
|
+
return bot.chooseAction(engine, state, seat);
|
|
205
|
+
}
|
|
206
|
+
if (state.phase === 'play' && state.extra.needSpade3 && seat === HUMAN) {
|
|
207
|
+
io.block(ui.skinActive()
|
|
208
|
+
? ui.skinMessage('first hand must contain spade-3 (3s)')
|
|
209
|
+
: colors.yellow(' 提醒:首手必须带上 ♠3'));
|
|
210
|
+
}
|
|
211
|
+
return askHuman(io, engine, state, seat, key);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let state;
|
|
215
|
+
let tries = 0;
|
|
216
|
+
do {
|
|
217
|
+
state = await runner.runDeal(engine, players, decider, null, {});
|
|
218
|
+
tries++;
|
|
219
|
+
} while (state.result && state.result.type === 'redeal' && tries < 5);
|
|
220
|
+
|
|
221
|
+
renderLocal(io, engine, state);
|
|
222
|
+
results.push(state);
|
|
223
|
+
round++;
|
|
224
|
+
|
|
225
|
+
const again = await new Promise(function (resolve) {
|
|
226
|
+
io.ask(ui.skinActive() ? ui.skinPrompt() : '\n再来一局?(y = 继续 / 其它 = 返回菜单) > ',
|
|
227
|
+
function (l) { resolve(/^y/i.test(l.trim())); });
|
|
228
|
+
});
|
|
229
|
+
if (!again) break;
|
|
230
|
+
}
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* ------------------------------------------------------------------ */
|
|
235
|
+
/* 菜单 */
|
|
236
|
+
/* ------------------------------------------------------------------ */
|
|
237
|
+
|
|
238
|
+
function banner() {
|
|
239
|
+
return [
|
|
240
|
+
'',
|
|
241
|
+
colors.bold(' dou-cl') + colors.dim(' v' + require('../package.json').version),
|
|
242
|
+
'',
|
|
243
|
+
].join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const MENU_SINGLE = [
|
|
247
|
+
['1', 'doudizhu', '斗地主', '3 人 · 17 张 + 3 张底牌'],
|
|
248
|
+
['2', 'paodekuai', '跑得快', '3 人 · 一副牌去掉大小王'],
|
|
249
|
+
['3', 'fivek', '510K', '4 人 · 两副牌 · 5/10/K 计分'],
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
function showMenu(io, opts) {
|
|
253
|
+
io.clear();
|
|
254
|
+
io.say(banner());
|
|
255
|
+
io.say(' ' + '\u2500'.repeat(58));
|
|
256
|
+
io.say(' ' + colors.bold('单机'));
|
|
257
|
+
MENU_SINGLE.forEach(function (m) {
|
|
258
|
+
io.say(' ' + colors.bold(m[0]) + ' ' + m[2] + ' ' + colors.dim(m[3]));
|
|
259
|
+
});
|
|
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') + ' 本机开服务器');
|
|
266
|
+
io.say('');
|
|
267
|
+
io.say(' ' + colors.bold('0') + ' 退出');
|
|
268
|
+
io.say(' ' + '\u2500'.repeat(58));
|
|
269
|
+
io.say(colors.dim(' 直接回车 = 1(单机斗地主)'));
|
|
270
|
+
|
|
271
|
+
io.ask('\n请选择 > ', function (line) {
|
|
272
|
+
const c = (line || '1').trim();
|
|
273
|
+
if (c === '0') { io.say('再见!'); io.close(); return; }
|
|
274
|
+
if (c === '1' || c === '2' || c === '3') {
|
|
275
|
+
const key = MENU_SINGLE[Number(c) - 1][1];
|
|
276
|
+
playLocal(io, key, opts).then(function () { showMenu(io, opts); })
|
|
277
|
+
.catch(function (e) { io.say(colors.red('出错: ' + e.message)); showMenu(io, opts); });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (c === '4' || c === '5' || c === '6') {
|
|
281
|
+
onlineFlow(io, String(Number(c) - 3), opts).catch(function (e) {
|
|
282
|
+
io.say(colors.red('出错: ' + e.message));
|
|
283
|
+
showMenu(io, opts);
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (c === '7') { startServerFlow(io, opts.port); return; }
|
|
288
|
+
io.say(colors.red('没有这个选项:' + c));
|
|
289
|
+
setTimeout(function () { showMenu(io, opts); }, 600);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function ask(io, prompt) {
|
|
294
|
+
return new Promise(function (resolve) {
|
|
295
|
+
io.ask(prompt, function (l) { resolve(l.trim()); });
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function onlineFlow(io, choice, opts) {
|
|
300
|
+
io.clear();
|
|
301
|
+
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
|
+
|
|
325
|
+
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;
|
|
330
|
+
io.close();
|
|
331
|
+
const client = require('./net/client');
|
|
332
|
+
await client.runClient(new ui.IO(), { mode: 'match', game: key, name: name, server: srv }, opts);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function startServerFlow(io, port) {
|
|
336
|
+
io.clear();
|
|
337
|
+
io.say('');
|
|
338
|
+
io.say(' 启动服务器…(Ctrl+C 停止)');
|
|
339
|
+
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)));
|
|
343
|
+
io.handler = null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/* ------------------------------------------------------------------ */
|
|
347
|
+
|
|
348
|
+
module.exports = {
|
|
349
|
+
playLocal: playLocal,
|
|
350
|
+
showMenu: showMenu,
|
|
351
|
+
printHelp: printHelp,
|
|
352
|
+
printStealthHelp: printStealthHelp,
|
|
353
|
+
printSkinHelp: printSkinHelp,
|
|
354
|
+
banner: banner,
|
|
355
|
+
ask: ask,
|
|
356
|
+
};
|