maid-poker-cli 1.0.0 → 1.0.1

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/README.md CHANGED
@@ -4,9 +4,12 @@ A command-line multiplayer Dou Dizhu (斗地主) game built with Node.js and Soc
4
4
 
5
5
  ## Features
6
6
  - **Multiplayer**: Host and join games over local network.
7
+ - **Two Game Modes**:
8
+ - 三人斗地主 (1副牌, 标准规则)
9
+ - 四人斗地主 (2副牌, 扩展规则)
7
10
  - **Beautiful UI**: ASCII art for cards, special effects for Bombs, Planes, and Rockets.
8
11
  - **Interactive**: Easy-to-use CLI interface.
9
- - **Rules**: Standard Dou Dizhu rules with robust card validation.
12
+ - **Rules**: Complete Dou Dizhu rules with robust card validation.
10
13
 
11
14
  ## Installation
12
15
 
@@ -22,14 +25,87 @@ maid-poker
22
25
  ```
23
26
 
24
27
  ### Hosting
25
- 1. Select "Host Game".
26
- 2. Share your IP address with friends.
27
- 3. Wait for players to join.
28
+ 1. Select "创建游戏 (Host Game)".
29
+ 2. Choose game mode (三人/四人斗地主).
30
+ 3. Share your IP address with friends.
31
+ 4. Wait for players to join.
28
32
 
29
33
  ### Joining
30
- 1. Select "Join Game".
34
+ 1. Select "加入游戏 (Join Game)".
31
35
  2. Enter the Host's IP address.
32
36
  3. Enjoy!
33
37
 
38
+ ## Game Modes
39
+
40
+ ### 三人斗地主 (Three Player Mode)
41
+ - **牌组**: 1副牌 (54张)
42
+ - **发牌**: 每人17张, 3张底牌
43
+ - **玩家**: 3人 (1地主 vs 2农民)
44
+
45
+ #### 牌型
46
+ | 牌型 | 说明 | 示例 |
47
+ |------|------|------|
48
+ | 单张 | 单张牌 | 7 |
49
+ | 对子 | 两张相同 | QQ |
50
+ | 三张 | 三张相同 | 888 |
51
+ | 三带一 | 三张 + 1张单牌 | 888+6 |
52
+ | 三带对 | 三张 + 1对 | 888+66 |
53
+ | 顺子 | 5张及以上连牌 (不含2和王) | 34567 |
54
+ | 连对 | 3对及以上连续对子 | 778899 |
55
+ | 飞机 | 2组及以上连续三张 | 333444 |
56
+ | 炸弹 | 4张相同 | 6666 |
57
+ | 王炸 | 大王 + 小王 (最强) | 🃏🃏 |
58
+
59
+ ### 四人斗地主 (Four Player Mode)
60
+ - **牌组**: 2副牌 (108张)
61
+ - **发牌**: 每人25张, 8张底牌
62
+ - **玩家**: 4人 (1地主 vs 3农民)
63
+
64
+ #### 新增/修改牌型
65
+ | 牌型 | 说明 | 示例 |
66
+ |------|------|------|
67
+ | 顺子 | 5张及以上连牌,**可含2**,不含王 | 34567, JQKA2 |
68
+ | 四带二 | 4张相同 + 2张单牌 | 9999+6+8 |
69
+ | 四带两对 | 4张相同 + 2对 | 9999+55+77 |
70
+ | 王炸 | 大王 + 小王 | 🃏🃏 |
71
+ | 八炸 | 8张相同牌 (2副牌独有) | 88888888 |
72
+ | 天王炸 | 4张王 (最强) | 🃏🃏🃏🃏 |
73
+
74
+ #### 炸弹大小顺序 (四人模式)
75
+ ```
76
+ 天王炸 (4王) > 八炸 (8张相同) > 王炸 (1大1小) > 普通炸弹 (4张相同)
77
+ ```
78
+
79
+ 同级别炸弹比较点数大小 (如 2222 > AAAA > KKKK...)
80
+
81
+ ## Development
82
+
83
+ ```bash
84
+ # Install dependencies
85
+ npm install
86
+
87
+ # Run in development mode
88
+ npm start
89
+
90
+ # Run tests
91
+ npm test
92
+
93
+ # Build for production
94
+ npm run build
95
+ ```
96
+
97
+ ## Testing
98
+
99
+ ```bash
100
+ # Run all tests
101
+ npm test
102
+
103
+ # Run tests in watch mode
104
+ npm run test:watch
105
+
106
+ # Run tests with coverage
107
+ npm run test:coverage
108
+ ```
109
+
34
110
  ## License
35
111
  ISC
package/dist/client.js CHANGED
@@ -23,6 +23,7 @@ class GameClient {
23
23
  constructor(serverUrl, name) {
24
24
  this.hand = [];
25
25
  this.myTurn = false;
26
+ this.gameMode = 'three_player';
26
27
  this.name = name;
27
28
  this.socket = (0, socket_io_client_1.io)(serverUrl);
28
29
  this.setupEvents();
@@ -32,15 +33,23 @@ class GameClient {
32
33
  console.log(chalk_1.default.green('已连接到服务器!'));
33
34
  this.socket.emit('join_game', this.name);
34
35
  });
35
- this.socket.on('player_list_update', (names) => {
36
- console.log(chalk_1.default.cyan(`\n当前玩家: ${names.join(', ')}`));
36
+ this.socket.on('game_mode', (data) => {
37
+ this.gameMode = data.mode;
38
+ const modeText = data.mode === 'three_player' ? '三人斗地主' : '四人斗地主(2副牌)';
39
+ console.log(chalk_1.default.magenta(`\n游戏模式: ${modeText} (需要 ${data.requiredPlayers} 位玩家)`));
40
+ });
41
+ this.socket.on('player_list_update', (data) => {
42
+ console.log(chalk_1.default.cyan(`\n当前玩家 (${data.names.length}/${data.required}): ${data.names.join(', ')}`));
37
43
  });
38
44
  this.socket.on('error_msg', (msg) => {
39
45
  console.log(chalk_1.default.red('错误:', msg));
40
46
  });
41
47
  this.socket.on('game_start', (data) => {
42
48
  console.clear();
49
+ this.gameMode = data.gameMode;
50
+ const modeText = data.gameMode === 'three_player' ? '三人斗地主' : '四人斗地主';
43
51
  console.log(chalk_1.default.green(figlet_1.default.textSync('Game Start!', { font: 'Standard' })));
52
+ console.log(chalk_1.default.magenta(`模式: ${modeText}`));
44
53
  this.hand = data.hand;
45
54
  this.printHand();
46
55
  console.log(chalk_1.default.yellow('等待叫分...'));
@@ -50,8 +59,9 @@ class GameClient {
50
59
  this.printHand();
51
60
  });
52
61
  this.socket.on('landlord_chosen', (data) => {
62
+ const cardCount = data.gameMode === 'three_player' ? 3 : 8;
53
63
  console.log(chalk_1.default.yellow(`\n地主是 ${data.landlordName}!`));
54
- console.log('底牌:');
64
+ console.log(`底牌 (${cardCount}张):`);
55
65
  console.log((0, view_1.renderCards)(data.hiddenCards));
56
66
  });
57
67
  this.socket.on('turn_update', (data) => {
@@ -61,64 +71,14 @@ class GameClient {
61
71
  }
62
72
  });
63
73
  this.socket.on('player_played', (data) => {
64
- // Clear screen to make it cleaner?
65
- // User requested: "出牌后清理上一轮命令行内容是否体验更好" -> Yes, but we need to redraw state.
66
- // But if we clear, we lose history of what happened.
67
- // Let's at least clear for the USER's OWN turn before input, or here if we want a fresh table.
68
- // Let's try clearing console before showing the played cards to focus attention.
69
74
  console.clear();
70
- // Check for special effects
71
75
  const { analyzeHand } = require('./game/logic');
72
- const analysis = analyzeHand(data.cards);
73
- if (analysis.type === 'rocket') {
74
- // Rocket (King Bomb)
75
- console.log(chalk_1.default.red.bold(figlet_1.default.textSync('ROCKET !!!', { font: 'Doom' })));
76
- console.log(chalk_1.default.bgRed.white.bold.italic(' 🚀 王 炸 🚀 '));
77
- }
78
- else if (analysis.type === 'bomb') {
79
- // Determine bomb intensity based on rank (3-15)
80
- const rank = analysis.rankValue;
81
- let bombText = 'BOMB';
82
- let fontToUse = 'Standard';
83
- if (rank <= 7) {
84
- // Small bomb (3-7)
85
- fontToUse = 'Small'; // Assuming 'Small' font exists or fallback
86
- }
87
- else if (rank <= 13) {
88
- // Medium bomb (8-K)
89
- fontToUse = 'Standard';
90
- }
91
- else {
92
- // Big bomb (A-2)
93
- fontToUse = 'Big';
94
- }
95
- // Use Standard/Big since 'Small' might be too small or not exist in standard figlet import
96
- // Let's stick to Standard vs Big vs Doom for simplicity
97
- if (rank > 13)
98
- fontToUse = 'Big';
99
- else
100
- fontToUse = 'Standard';
101
- console.log(chalk_1.default.red.bold(figlet_1.default.textSync(bombText, { font: fontToUse })));
102
- // Add magnitude indicator
103
- // Add magnitude indicator
104
- const stars = '⭐'.repeat(Math.min(5, Math.max(1, rank - 2)));
105
- const r = rank === 11 ? 'J' : rank === 12 ? 'Q' : rank === 13 ? 'K' : rank === 14 ? 'A' : rank === 15 ? '2' : rank.toString();
106
- console.log(chalk_1.default.bgRed.white.bold(` 💣 炸 弹 (${r}) 💣 `));
107
- }
108
- else if (analysis.type === 'airplane') {
109
- // Airplane effect
110
- console.log(chalk_1.default.cyan.bold(figlet_1.default.textSync('PLANE', { font: 'Standard' })));
111
- console.log(chalk_1.default.bgBlue.white.bold(' ✈️ 飞 机 ✈️ '));
112
- }
113
- else if (analysis.type === 'straight') {
114
- // Straight effect
115
- console.log(chalk_1.default.green.bold(figlet_1.default.textSync('STRAIGHT', { font: 'Standard' })));
116
- console.log(chalk_1.default.bgGreen.white.bold(' 🌊 顺 子 🌊 '));
117
- }
76
+ const analysis = analyzeHand(data.cards, data.gameMode);
77
+ // 特效显示
78
+ this.showHandEffect(analysis, data.gameMode);
118
79
  console.log(chalk_1.default.bold(`\n${data.playerName} 打出了:`));
119
80
  console.log((0, view_1.renderCards)(data.cards));
120
81
  console.log(chalk_1.default.gray(`剩余 ${data.remainingCount} 张牌.`));
121
- // Re-display my hand so context is not lost after clear
122
82
  this.printHand();
123
83
  });
124
84
  this.socket.on('player_passed', (data) => {
@@ -129,10 +89,8 @@ class GameClient {
129
89
  });
130
90
  this.socket.on('game_over', (data) => __awaiter(this, void 0, void 0, function* () {
131
91
  console.log(chalk_1.default.magenta(figlet_1.default.textSync('Game Over')));
132
- // data.role is 'landlord' or 'peasant'
133
92
  const roleText = data.role === 'landlord' ? '地主' : '农民';
134
93
  console.log(chalk_1.default.yellow(`赢家: ${data.winner} (${roleText} 胜利!)`));
135
- // Replay Prompt
136
94
  const { action } = yield inquirer_1.default.prompt([{
137
95
  type: 'list',
138
96
  name: 'action',
@@ -150,9 +108,62 @@ class GameClient {
150
108
  }));
151
109
  this.socket.on('your_turn', (data) => __awaiter(this, void 0, void 0, function* () {
152
110
  this.myTurn = true;
111
+ this.gameMode = data.gameMode;
153
112
  setTimeout(() => this.handleTurn(data.action), 500);
154
113
  }));
155
114
  }
115
+ showHandEffect(analysis, gameMode) {
116
+ const rank = analysis.rankValue;
117
+ const r = rank === 11 ? 'J' : rank === 12 ? 'Q' : rank === 13 ? 'K' : rank === 14 ? 'A' : rank === 15 ? '2' : rank.toString();
118
+ switch (analysis.type) {
119
+ case 'quad_joker':
120
+ // 天王炸 (4王)
121
+ console.log(chalk_1.default.red.bold(figlet_1.default.textSync('QUAD JOKER', { font: 'Doom' })));
122
+ console.log(chalk_1.default.bgRed.white.bold.italic(' 👑👑 天 王 炸 👑👑 '));
123
+ console.log(chalk_1.default.yellow(' 💥💥💥💥 '));
124
+ break;
125
+ case 'octo_bomb':
126
+ // 八炸 (8张相同)
127
+ console.log(chalk_1.default.red.bold(figlet_1.default.textSync('OCTO BOMB', { font: 'Doom' })));
128
+ console.log(chalk_1.default.bgRed.white.bold.italic(` 🔥🔥 八 炸 (${r}) 🔥🔥 `));
129
+ console.log(chalk_1.default.yellow(' 💣💣💣💣💣💣💣💣 '));
130
+ break;
131
+ case 'rocket':
132
+ // 王炸 (1大1小) - 三人和四人模式通用
133
+ console.log(chalk_1.default.red.bold(figlet_1.default.textSync('ROCKET !!!', { font: 'Doom' })));
134
+ console.log(chalk_1.default.bgRed.white.bold.italic(' 🚀 王 炸 🚀 '));
135
+ break;
136
+ case 'bomb':
137
+ // 普通炸弹
138
+ let fontToUse = 'Standard';
139
+ if (rank > 13)
140
+ fontToUse = 'Big';
141
+ console.log(chalk_1.default.red.bold(figlet_1.default.textSync('BOMB', { font: fontToUse })));
142
+ const stars = '⭐'.repeat(Math.min(5, Math.max(1, rank - 2)));
143
+ console.log(chalk_1.default.bgRed.white.bold(` 💣 炸 弹 (${r}) 💣 `));
144
+ break;
145
+ case 'airplane':
146
+ console.log(chalk_1.default.cyan.bold(figlet_1.default.textSync('PLANE', { font: 'Standard' })));
147
+ console.log(chalk_1.default.bgBlue.white.bold(' ✈️ 飞 机 ✈️ '));
148
+ break;
149
+ case 'straight':
150
+ console.log(chalk_1.default.green.bold(figlet_1.default.textSync('STRAIGHT', { font: 'Standard' })));
151
+ console.log(chalk_1.default.bgGreen.white.bold(' 🌊 顺 子 🌊 '));
152
+ break;
153
+ case 'consecutive_pairs':
154
+ console.log(chalk_1.default.blue.bold(figlet_1.default.textSync('PAIRS', { font: 'Standard' })));
155
+ console.log(chalk_1.default.bgBlue.white.bold(' 🔗 连 对 🔗 '));
156
+ break;
157
+ case 'four_with_two':
158
+ console.log(chalk_1.default.yellow.bold(figlet_1.default.textSync('4+2', { font: 'Standard' })));
159
+ console.log(chalk_1.default.bgYellow.black.bold(` 💪 四带二 (${r}) 💪 `));
160
+ break;
161
+ case 'four_with_pairs':
162
+ console.log(chalk_1.default.yellow.bold(figlet_1.default.textSync('4+2x2', { font: 'Standard' })));
163
+ console.log(chalk_1.default.bgYellow.black.bold(` 💪 四带两对 (${r}) 💪 `));
164
+ break;
165
+ }
166
+ }
156
167
  colorCard(c) {
157
168
  const str = (0, poker_1.formatCard)(c);
158
169
  if (c.suit === '♥' || c.suit === '♦' || c.rank === 'RJ') {
@@ -163,7 +174,8 @@ class GameClient {
163
174
  }
164
175
  }
165
176
  printHand() {
166
- console.log(chalk_1.default.blue('\n你的手牌:'));
177
+ const handCount = this.hand.length;
178
+ console.log(chalk_1.default.blue(`\n你的手牌 (${handCount}张):`));
167
179
  console.log((0, view_1.renderCards)(this.hand));
168
180
  console.log('\n');
169
181
  }
@@ -179,7 +191,6 @@ class GameClient {
179
191
  this.socket.emit('bid_landlord', bid ? 1 : 0);
180
192
  }
181
193
  else {
182
- // Updated to use input for cards
183
194
  const { cardInput } = yield inquirer_1.default.prompt([{
184
195
  type: 'input',
185
196
  name: 'cardInput',
@@ -212,25 +223,17 @@ class GameClient {
212
223
  this.myTurn = false;
213
224
  });
214
225
  }
215
- // Helper to match input string to hand cards
216
- // Supports "34567", "3344", "3 4 5", "10 J Q K"
217
226
  parseCards(input) {
218
- const tempHand = [...this.hand]; // Copy to track usage
227
+ const tempHand = [...this.hand];
219
228
  const selected = [];
220
229
  let tokens = [];
221
- // Revised parsing logic:
222
- // Parse left-to-right, looking for valid tokens.
223
- // Priority: Multi-char tokens (10, BJ, RJ) > Single-char tokens.
224
- // Aliases: 0 -> 10, T -> 10.
225
230
  let remaining = input.toUpperCase();
226
231
  while (remaining.length > 0) {
227
- // Skip separators
228
232
  if (/^[\s,]+/.test(remaining)) {
229
233
  remaining = remaining.replace(/^[\s,]+/, '');
230
234
  continue;
231
235
  }
232
236
  let foundToken = '';
233
- // 1. Check for specific multi-char tokens aliases first
234
237
  if (remaining.startsWith('10')) {
235
238
  foundToken = '10';
236
239
  remaining = remaining.substring(2);
@@ -243,7 +246,6 @@ class GameClient {
243
246
  foundToken = 'RJ';
244
247
  remaining = remaining.substring(2);
245
248
  }
246
- // 2. Check for Single char aliases for 10
247
249
  else if (remaining.startsWith('0')) {
248
250
  foundToken = '10';
249
251
  remaining = remaining.substring(1);
@@ -252,7 +254,6 @@ class GameClient {
252
254
  foundToken = '10';
253
255
  remaining = remaining.substring(1);
254
256
  }
255
- // 3. Check for standard single chars
256
257
  else {
257
258
  const char = remaining[0];
258
259
  if (poker_1.RANKS.includes(char)) {
@@ -260,7 +261,6 @@ class GameClient {
260
261
  remaining = remaining.substring(1);
261
262
  }
262
263
  else {
263
- // Invalid character found
264
264
  return null;
265
265
  }
266
266
  }
@@ -268,15 +268,11 @@ class GameClient {
268
268
  tokens.push(foundToken);
269
269
  }
270
270
  }
271
- // Now maps tokens to cards in hand
272
271
  for (const token of tokens) {
273
- // Token is already normalized rank string (e.g. '10', 'A', 'BJ')
274
272
  const idx = tempHand.findIndex(c => c.rank === token);
275
273
  if (idx === -1) {
276
- // Not found in hand
277
274
  return null;
278
275
  }
279
- // Remove from tempHand to avoid double counting same card
280
276
  selected.push(tempHand[idx]);
281
277
  tempHand.splice(idx, 1);
282
278
  }