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.
@@ -2,36 +2,74 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.analyzeHand = analyzeHand;
4
4
  exports.canBeat = canBeat;
5
- function analyzeHand(cards) {
5
+ exports.getHandTypeForEffect = getHandTypeForEffect;
6
+ // 获取牌的频率统计
7
+ function getCardCounts(cards) {
8
+ const counts = {};
9
+ cards.forEach(c => counts[c.value] = (counts[c.value] || 0) + 1);
10
+ return counts;
11
+ }
12
+ // 统计王的数量
13
+ function countJokers(cards) {
14
+ let big = 0, small = 0;
15
+ cards.forEach(c => {
16
+ if (c.value === 17)
17
+ big++;
18
+ if (c.value === 16)
19
+ small++;
20
+ });
21
+ return { big, small };
22
+ }
23
+ function analyzeHand(cards, gameMode = 'three_player') {
6
24
  const len = cards.length;
7
25
  if (len === 0)
8
26
  return { type: 'invalid', rankValue: 0, length: 0 };
9
27
  // Sort descending
10
28
  cards.sort((a, b) => b.value - a.value);
11
- // Check Rocket (Red Joker + Black Joker)
12
- if (len === 2 && cards[0].value === 17 && cards[1].value === 16) {
13
- return { type: 'rocket', rankValue: 100, length: 2 };
29
+ const counts = getCardCounts(cards);
30
+ const jokers = countJokers(cards);
31
+ const totalJokers = jokers.big + jokers.small;
32
+ // ====== 四人模式特殊牌型 ======
33
+ if (gameMode === 'four_player') {
34
+ // 天王炸(4王,最强)- 2大王+2小王
35
+ if (len === 4 && totalJokers === 4) {
36
+ return { type: 'quad_joker', rankValue: 200, length: 4, bombLevel: 4 };
37
+ }
38
+ // 八炸(8张相同)
39
+ if (len === 8) {
40
+ const vals = Object.keys(counts).map(Number);
41
+ if (vals.length === 1 && counts[vals[0]] === 8) {
42
+ return { type: 'octo_bomb', rankValue: vals[0], length: 8, bombLevel: 3 };
43
+ }
44
+ }
45
+ // 普通王炸(1大1小)- 四人模式下等级为2,介于普通炸弹和八炸之间
46
+ if (len === 2 && jokers.big >= 1 && jokers.small >= 1) {
47
+ return { type: 'rocket', rankValue: 100, length: 2, bombLevel: 2 };
48
+ }
49
+ }
50
+ // ====== 通用牌型(三人和四人模式共享) ======
51
+ // 普通王炸(三人模式:1大1小;四人模式不适用这里,已在上面处理)
52
+ if (gameMode === 'three_player' && len === 2 && cards[0].value === 17 && cards[1].value === 16) {
53
+ return { type: 'rocket', rankValue: 100, length: 2, bombLevel: 5 }; // 三人模式王炸最大
14
54
  }
15
- // Check Bomb (4 same rank)
55
+ // 普通炸弹(4张相同)
16
56
  if (len === 4 && cards[0].value === cards[3].value) {
17
- return { type: 'bomb', rankValue: cards[0].value, length: 4 };
57
+ return { type: 'bomb', rankValue: cards[0].value, length: 4, bombLevel: 1 };
18
58
  }
19
- // Single
59
+ // 单张
20
60
  if (len === 1) {
21
61
  return { type: 'single', rankValue: cards[0].value, length: 1 };
22
62
  }
23
- // Pair
63
+ // 对子
24
64
  if (len === 2 && cards[0].value === cards[1].value) {
25
65
  return { type: 'pair', rankValue: cards[0].value, length: 2 };
26
66
  }
27
- // Triple
67
+ // 三张
28
68
  if (len === 3 && cards[0].value === cards[2].value) {
29
69
  return { type: 'triple', rankValue: cards[0].value, length: 3 };
30
70
  }
31
- // Triple + One
71
+ // 三带一
32
72
  if (len === 4) {
33
- // 3 same + 1 diff. Because sorted, 3 same must be at start or end.
34
- // AAA B or B AAA
35
73
  if (cards[0].value === cards[2].value) { // AAA B
36
74
  return { type: 'triple_one', rankValue: cards[0].value, length: 4 };
37
75
  }
@@ -39,9 +77,8 @@ function analyzeHand(cards) {
39
77
  return { type: 'triple_one', rankValue: cards[1].value, length: 4 };
40
78
  }
41
79
  }
42
- // Triple + Pair
80
+ // 三带对
43
81
  if (len === 5) {
44
- // AAA BB or BB AAA
45
82
  if (cards[0].value === cards[2].value && cards[3].value === cards[4].value) { // AAA BB
46
83
  return { type: 'triple_pair', rankValue: cards[0].value, length: 5 };
47
84
  }
@@ -49,29 +86,96 @@ function analyzeHand(cards) {
49
86
  return { type: 'triple_pair', rankValue: cards[2].value, length: 5 };
50
87
  }
51
88
  }
52
- // Straight (顺子): 5+ cards, consecutive values, max rank A (14). 2 (15) and Jokers (16,17) cannot be in straight.
89
+ // ====== 四带二(四人模式) ======
90
+ if (gameMode === 'four_player' && len === 6) {
91
+ const vals = Object.keys(counts).map(Number);
92
+ const fourVal = vals.find(v => counts[v] === 4);
93
+ if (fourVal !== undefined) {
94
+ // 找到4张相同的牌,剩下2张是单牌
95
+ const others = vals.filter(v => v !== fourVal);
96
+ if (others.length === 2 || (others.length === 1 && counts[others[0]] === 2)) {
97
+ return { type: 'four_with_two', rankValue: fourVal, length: 6 };
98
+ }
99
+ }
100
+ }
101
+ // 四带两对(四人模式)
102
+ if (gameMode === 'four_player' && len === 8) {
103
+ const vals = Object.keys(counts).map(Number);
104
+ const fourVal = vals.find(v => counts[v] === 4);
105
+ if (fourVal !== undefined) {
106
+ const others = vals.filter(v => v !== fourVal);
107
+ // 检查剩余是否都是对子
108
+ const allPairs = others.every(v => counts[v] === 2);
109
+ if (allPairs && others.length === 2) {
110
+ return { type: 'four_with_pairs', rankValue: fourVal, length: 8 };
111
+ }
112
+ }
113
+ }
114
+ // ====== 顺子 ======
115
+ // 三人模式:5张及以上连牌,不含2和王
116
+ // 四人模式:5张及以上连牌,可含2,不含王(如 A2345, 10JQKA 都可以)
53
117
  if (len >= 5) {
54
- const isStraight = cards.every((c, i) => {
55
- if (i === 0)
56
- return true;
57
- return c.value === cards[i - 1].value - 1;
58
- });
59
- // Check max value is not 2 or Joker
60
- // cards are sorted desc, so cards[0] is max
61
- if (isStraight && cards[0].value < 15) {
62
- return { type: 'straight', rankValue: cards[0].value, length: len };
118
+ // 检查是否包含王
119
+ const hasJoker = cards.some(c => c.value >= 16);
120
+ if (!hasJoker) {
121
+ let isStraight = true;
122
+ // 四人模式特殊处理:支持包含2的顺子
123
+ if (gameMode === 'four_player') {
124
+ // 检查是否是 A2345 这种情况(A作为1使用)
125
+ const sortedValues = cards.map(c => c.value).sort((a, b) => a - b);
126
+ const minVal = sortedValues[0];
127
+ const maxVal = sortedValues[sortedValues.length - 1];
128
+ // 正常顺子检查
129
+ for (let i = 0; i < len - 1; i++) {
130
+ if (sortedValues[i + 1] - sortedValues[i] !== 1) {
131
+ isStraight = false;
132
+ break;
133
+ }
134
+ }
135
+ // 检查 A2345 (值: 3,4,5,14,15 -> 需要把A视为1)
136
+ if (!isStraight && sortedValues.includes(14) && sortedValues.includes(15)) {
137
+ const adjustedValues = sortedValues.map(v => v === 14 ? 1 : v === 15 ? 2 : v).sort((a, b) => a - b);
138
+ isStraight = true;
139
+ for (let i = 0; i < len - 1; i++) {
140
+ if (adjustedValues[i + 1] - adjustedValues[i] !== 1) {
141
+ isStraight = false;
142
+ break;
143
+ }
144
+ }
145
+ if (isStraight) {
146
+ return { type: 'straight', rankValue: 15, length: len }; // 2是最大值
147
+ }
148
+ }
149
+ }
150
+ else {
151
+ // 三人模式:不能包含2
152
+ if (cards[0].value >= 15) {
153
+ isStraight = false;
154
+ }
155
+ else {
156
+ for (let i = 0; i < len - 1; i++) {
157
+ if (cards[i].value - cards[i + 1].value !== 1) {
158
+ isStraight = false;
159
+ break;
160
+ }
161
+ }
162
+ }
163
+ }
164
+ if (isStraight && cards[0].value < 16) {
165
+ return { type: 'straight', rankValue: cards[0].value, length: len };
166
+ }
63
167
  }
64
168
  }
65
- // Consecutive Pairs (连对): 3+ pairs (6+ cards), consecutive pair values. No 2, No Joker.
169
+ // ====== 连对 ======
170
+ // 三人模式:3对及以上连对,不含2和王
171
+ // 四人模式:3对及以上连对
66
172
  if (len >= 6 && len % 2 === 0) {
67
173
  let isConsecutivePairs = true;
68
174
  for (let i = 0; i < len; i += 2) {
69
- // Check pair
70
175
  if (cards[i].value !== cards[i + 1].value) {
71
176
  isConsecutivePairs = false;
72
177
  break;
73
178
  }
74
- // Check consecutive with previous pair
75
179
  if (i > 0) {
76
180
  if (cards[i].value !== cards[i - 2].value - 1) {
77
181
  isConsecutivePairs = false;
@@ -79,59 +183,112 @@ function analyzeHand(cards) {
79
183
  }
80
184
  }
81
185
  }
82
- if (isConsecutivePairs && cards[0].value < 15) {
186
+ // 三人模式不能包含2
187
+ const maxVal = gameMode === 'three_player' ? 14 : 15;
188
+ if (isConsecutivePairs && cards[0].value <= maxVal) {
83
189
  return { type: 'consecutive_pairs', rankValue: cards[0].value, length: len };
84
190
  }
85
191
  }
86
- // Airplane (飞机): Consecutive Triples (e.g., 333444).
87
- // Simplify: Just check strictly consecutive triples without wings for this detector,
88
- // or heuristic: if we find consecutive triples, we call it airplane for effect.
89
- // Let's do a basic frequency map check.
90
- const counts = {};
91
- cards.forEach(c => counts[c.value] = (counts[c.value] || 0) + 1);
192
+ // ====== 飞机 ======
193
+ // 检测连续三张(可带翅膀)
92
194
  const triples = [];
93
195
  for (const val in counts) {
94
196
  if (counts[val] >= 3)
95
197
  triples.push(Number(val));
96
198
  }
97
- triples.sort((a, b) => b - a); // Descending
98
- // Check for consecutive triples
99
- let consecutiveTriples = 0;
100
- for (let i = 0; i < triples.length - 1; i++) {
101
- if (triples[i] === triples[i + 1] + 1 && triples[i] < 15) { // No 2/Jokers in airplane
102
- consecutiveTriples++;
199
+ triples.sort((a, b) => b - a);
200
+ // 检查连续三张的数量
201
+ if (triples.length >= 2) {
202
+ // 找最长的连续三张序列
203
+ let maxConsecutive = 1;
204
+ let currentConsecutive = 1;
205
+ let startTriple = triples[0];
206
+ for (let i = 0; i < triples.length - 1; i++) {
207
+ const maxAllowed = gameMode === 'three_player' ? 14 : 15; // 三人模式不含2
208
+ if (triples[i] <= maxAllowed && triples[i] === triples[i + 1] + 1) {
209
+ currentConsecutive++;
210
+ if (currentConsecutive > maxConsecutive) {
211
+ maxConsecutive = currentConsecutive;
212
+ startTriple = triples[i - maxConsecutive + 2];
213
+ }
214
+ }
215
+ else {
216
+ currentConsecutive = 1;
217
+ }
218
+ }
219
+ if (maxConsecutive >= 2) {
220
+ // 验证翅膀数量
221
+ const tripleCount = maxConsecutive;
222
+ const expectedLen = tripleCount * 3; // 无翅膀
223
+ const expectedLenWithSingles = tripleCount * 4; // 带单
224
+ const expectedLenWithPairs = tripleCount * 5; // 带对
225
+ if (len === expectedLen || len === expectedLenWithSingles || len === expectedLenWithPairs) {
226
+ return { type: 'airplane', rankValue: startTriple, length: len };
227
+ }
103
228
  }
104
- }
105
- // minimal airplane is 2 consecutive triples
106
- if (consecutiveTriples >= 1) {
107
- // This is a loose check, mostly for the Visual Effect triggering
108
- // Since we want to show "Airplane" effect.
109
- return { type: 'airplane', rankValue: triples[0], length: len };
110
229
  }
111
230
  return { type: 'invalid', rankValue: 0, length: 0 };
112
231
  }
113
- function canBeat(lastHand, newHand) {
114
- const last = analyzeHand(lastHand);
115
- const curr = analyzeHand(newHand);
232
+ function canBeat(lastHand, newHand, gameMode = 'three_player') {
233
+ const last = analyzeHand(lastHand, gameMode);
234
+ const curr = analyzeHand(newHand, gameMode);
116
235
  if (curr.type === 'invalid')
117
236
  return false;
118
- // Rocket beats everything
119
- if (curr.type === 'rocket')
237
+ // ====== 炸弹比较逻辑 ======
238
+ const isBombType = (type) => ['bomb', 'rocket', 'quad_joker', 'octo_bomb'].includes(type);
239
+ const currIsBomb = isBombType(curr.type);
240
+ const lastIsBomb = isBombType(last.type);
241
+ if (currIsBomb && lastIsBomb) {
242
+ // 都是炸弹类型,比较炸弹级别
243
+ // 炸弹等级:普通炸(1) < 双王炸(2) < 八炸(3) < 天王炸(4) ; 三人王炸(5)
244
+ const currLevel = curr.bombLevel || 0;
245
+ const lastLevel = last.bombLevel || 0;
246
+ if (currLevel !== lastLevel) {
247
+ return currLevel > lastLevel;
248
+ }
249
+ // 同级别炸弹比较点数
250
+ return curr.rankValue > last.rankValue;
251
+ }
252
+ // 炸弹类型打非炸弹类型
253
+ if (currIsBomb && !lastIsBomb) {
120
254
  return true;
121
- if (last.type === 'rocket')
255
+ }
256
+ // 非炸弹类型不能打炸弹类型
257
+ if (!currIsBomb && lastIsBomb) {
122
258
  return false;
123
- // Bomb beats everything except rocket and bigger bomb
124
- if (curr.type === 'bomb') {
125
- if (last.type === 'bomb')
126
- return curr.rankValue > last.rankValue;
127
- return true;
128
259
  }
129
- if (last.type === 'bomb')
130
- return false; // Current is not bomb/rocket
131
- // Otherwise, types must match and lengths must match (usually)
260
+ // ====== 非炸弹牌型比较 ======
261
+ // 类型必须相同
132
262
  if (curr.type !== last.type)
133
263
  return false;
264
+ // 长度必须相同
134
265
  if (curr.length !== last.length)
135
266
  return false;
136
267
  return curr.rankValue > last.rankValue;
137
268
  }
269
+ // 导出用于特效判断
270
+ function getHandTypeForEffect(cards, gameMode = 'three_player') {
271
+ const analysis = analyzeHand(cards, gameMode);
272
+ const typeNames = {
273
+ 'single': '单张',
274
+ 'pair': '对子',
275
+ 'triple': '三张',
276
+ 'triple_one': '三带一',
277
+ 'triple_pair': '三带对',
278
+ 'four_with_two': '四带二',
279
+ 'four_with_pairs': '四带两对',
280
+ 'bomb': '炸弹',
281
+ 'rocket': gameMode === 'four_player' ? '双王炸' : '王炸',
282
+ 'quad_joker': '天王炸',
283
+ 'octo_bomb': '八炸',
284
+ 'straight': '顺子',
285
+ 'consecutive_pairs': '连对',
286
+ 'airplane': '飞机',
287
+ 'invalid': '无效'
288
+ };
289
+ return {
290
+ type: analysis.type,
291
+ rankValue: analysis.rankValue,
292
+ displayName: typeNames[analysis.type]
293
+ };
294
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RANKS = void 0;
4
4
  exports.getCardValue = getCardValue;
5
5
  exports.createDeck = createDeck;
6
+ exports.createDoubleDeck = createDoubleDeck;
6
7
  exports.shuffleDeck = shuffleDeck;
7
8
  exports.sortHand = sortHand;
8
9
  exports.formatCard = formatCard;
@@ -28,6 +29,24 @@ function createDeck() {
28
29
  deck.push({ suit: '', rank: 'RJ', value: 17 });
29
30
  return deck;
30
31
  }
32
+ // 创建双副牌(用于四人斗地主,共108张牌)
33
+ function createDoubleDeck() {
34
+ const suits = ['♠', '♥', '♣', '♦'];
35
+ const normalRanks = exports.RANKS.slice(0, 13); // 3 to 2
36
+ let deck = [];
37
+ // 两副牌
38
+ for (let deckIdx = 0; deckIdx < 2; deckIdx++) {
39
+ for (const r of normalRanks) {
40
+ for (const s of suits) {
41
+ deck.push({ suit: s, rank: r, value: getCardValue(r), deckIndex: deckIdx });
42
+ }
43
+ }
44
+ // Jokers
45
+ deck.push({ suit: '', rank: 'BJ', value: 16, deckIndex: deckIdx });
46
+ deck.push({ suit: '', rank: 'RJ', value: 17, deckIndex: deckIdx });
47
+ }
48
+ return deck; // 共 108 张牌
49
+ }
31
50
  function shuffleDeck(deck) {
32
51
  for (let i = deck.length - 1; i > 0; i--) {
33
52
  const j = Math.floor(Math.random() * (i + 1));
package/dist/index.js CHANGED
@@ -45,10 +45,41 @@ function main() {
45
45
  }
46
46
  ]);
47
47
  if (mode.includes('Host')) {
48
+ // 选择游戏模式
49
+ const { gameMode } = yield inquirer_1.default.prompt([
50
+ {
51
+ type: 'list',
52
+ name: 'gameMode',
53
+ message: '请选择游戏模式:',
54
+ choices: [
55
+ { name: '三人斗地主 (1副牌, 每人17张)', value: 'three_player' },
56
+ { name: '四人斗地主 (2副牌, 每人25张)', value: 'four_player' }
57
+ ]
58
+ }
59
+ ]);
60
+ const selectedMode = gameMode;
61
+ const requiredPlayers = selectedMode === 'three_player' ? 3 : 4;
62
+ const modeText = selectedMode === 'three_player' ? '三人斗地主' : '四人斗地主(2副牌)';
63
+ console.log(chalk_1.default.magenta(`\n已选择: ${modeText}`));
64
+ console.log(chalk_1.default.gray(`需要 ${requiredPlayers} 位玩家参与\n`));
65
+ // 显示规则提示
66
+ if (selectedMode === 'four_player') {
67
+ console.log(chalk_1.default.yellow('=== 四人斗地主规则 ==='));
68
+ console.log(chalk_1.default.gray('• 使用2副牌(108张),每人25张,8张底牌'));
69
+ console.log(chalk_1.default.gray('• 顺子可含2 (如 A2345, 10JQKA)'));
70
+ console.log(chalk_1.default.gray('• 新增牌型:'));
71
+ console.log(chalk_1.default.cyan(' - 四带二: 4张相同 + 2张单牌'));
72
+ console.log(chalk_1.default.cyan(' - 四带两对: 4张相同 + 2对'));
73
+ console.log(chalk_1.default.cyan(' - 双王炸: 2大王 + 2小王'));
74
+ console.log(chalk_1.default.cyan(' - 八炸: 8张相同牌'));
75
+ console.log(chalk_1.default.cyan(' - 天王炸: 4张王 (最强)'));
76
+ console.log(chalk_1.default.gray('• 炸弹大小: 天王炸 > 八炸 > 双王炸 > 普通炸弹'));
77
+ console.log('');
78
+ }
48
79
  const ips = getLocalIPs();
49
80
  console.log(chalk_1.default.blue(`\n服务器已启动,请尝试以下IP: \n${ips.map(ip => ` - ${ip}`).join('\n')}`));
50
- // Start the server
51
- new server_1.GameServer();
81
+ // Start the server with selected game mode
82
+ new server_1.GameServer(selectedMode);
52
83
  const { joinAsPlayer } = yield inquirer_1.default.prompt([{
53
84
  type: 'confirm',
54
85
  name: 'joinAsPlayer',