maid-poker-cli 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/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # Maid Poker (女仆扑克牌)
2
+
3
+ A command-line multiplayer Dou Dizhu (斗地主) game built with Node.js and Socket.io.
4
+
5
+ ## Features
6
+ - **Multiplayer**: Host and join games over local network.
7
+ - **Beautiful UI**: ASCII art for cards, special effects for Bombs, Planes, and Rockets.
8
+ - **Interactive**: Easy-to-use CLI interface.
9
+ - **Rules**: Standard Dou Dizhu rules with robust card validation.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install -g maid-poker-cli
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Start the game:
20
+ ```bash
21
+ maid-poker
22
+ ```
23
+
24
+ ### Hosting
25
+ 1. Select "Host Game".
26
+ 2. Share your IP address with friends.
27
+ 3. Wait for players to join.
28
+
29
+ ### Joining
30
+ 1. Select "Join Game".
31
+ 2. Enter the Host's IP address.
32
+ 3. Enjoy!
33
+
34
+ ## License
35
+ ISC
package/dist/client.js ADDED
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.GameClient = void 0;
16
+ const socket_io_client_1 = require("socket.io-client");
17
+ const inquirer_1 = __importDefault(require("inquirer"));
18
+ const chalk_1 = __importDefault(require("chalk"));
19
+ const figlet_1 = __importDefault(require("figlet"));
20
+ const poker_1 = require("./game/poker");
21
+ const view_1 = require("./game/view");
22
+ class GameClient {
23
+ constructor(serverUrl, name) {
24
+ this.hand = [];
25
+ this.myTurn = false;
26
+ this.name = name;
27
+ this.socket = (0, socket_io_client_1.io)(serverUrl);
28
+ this.setupEvents();
29
+ }
30
+ setupEvents() {
31
+ this.socket.on('connect', () => {
32
+ console.log(chalk_1.default.green('已连接到服务器!'));
33
+ this.socket.emit('join_game', this.name);
34
+ });
35
+ this.socket.on('player_list_update', (names) => {
36
+ console.log(chalk_1.default.cyan(`\n当前玩家: ${names.join(', ')}`));
37
+ });
38
+ this.socket.on('error_msg', (msg) => {
39
+ console.log(chalk_1.default.red('错误:', msg));
40
+ });
41
+ this.socket.on('game_start', (data) => {
42
+ console.clear();
43
+ console.log(chalk_1.default.green(figlet_1.default.textSync('Game Start!', { font: 'Standard' })));
44
+ this.hand = data.hand;
45
+ this.printHand();
46
+ console.log(chalk_1.default.yellow('等待叫分...'));
47
+ });
48
+ this.socket.on('hand_update', (newHand) => {
49
+ this.hand = newHand;
50
+ this.printHand();
51
+ });
52
+ this.socket.on('landlord_chosen', (data) => {
53
+ console.log(chalk_1.default.yellow(`\n地主是 ${data.landlordName}!`));
54
+ console.log('底牌:');
55
+ console.log((0, view_1.renderCards)(data.hiddenCards));
56
+ });
57
+ this.socket.on('turn_update', (data) => {
58
+ if (data.playerName !== this.name) {
59
+ const actionText = data.action === 'bid' ? '叫分' : '出牌';
60
+ console.log(chalk_1.default.gray(`\n等待 ${data.playerName} ${actionText}...`));
61
+ }
62
+ });
63
+ 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
+ console.clear();
70
+ // Check for special effects
71
+ 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
+ }
118
+ console.log(chalk_1.default.bold(`\n${data.playerName} 打出了:`));
119
+ console.log((0, view_1.renderCards)(data.cards));
120
+ console.log(chalk_1.default.gray(`剩余 ${data.remainingCount} 张牌.`));
121
+ // Re-display my hand so context is not lost after clear
122
+ this.printHand();
123
+ });
124
+ this.socket.on('player_passed', (data) => {
125
+ console.log(chalk_1.default.gray(`\n${data.playerName} 要不起.`));
126
+ });
127
+ this.socket.on('new_round', (data) => {
128
+ console.log(chalk_1.default.green(`\n${data.playerName} 获得新回合!`));
129
+ });
130
+ this.socket.on('game_over', (data) => __awaiter(this, void 0, void 0, function* () {
131
+ console.log(chalk_1.default.magenta(figlet_1.default.textSync('Game Over')));
132
+ // data.role is 'landlord' or 'peasant'
133
+ const roleText = data.role === 'landlord' ? '地主' : '农民';
134
+ console.log(chalk_1.default.yellow(`赢家: ${data.winner} (${roleText} 胜利!)`));
135
+ // Replay Prompt
136
+ const { action } = yield inquirer_1.default.prompt([{
137
+ type: 'list',
138
+ name: 'action',
139
+ message: '本局结束,你想做什么?',
140
+ choices: ['继续游戏 (Play Again)', '退出 (Exit)']
141
+ }]);
142
+ if (action.includes('Exit')) {
143
+ process.exit(0);
144
+ }
145
+ else {
146
+ console.clear();
147
+ console.log(chalk_1.default.green('等待其他玩家准备...'));
148
+ this.socket.emit('player_ready');
149
+ }
150
+ }));
151
+ this.socket.on('your_turn', (data) => __awaiter(this, void 0, void 0, function* () {
152
+ this.myTurn = true;
153
+ setTimeout(() => this.handleTurn(data.action), 500);
154
+ }));
155
+ }
156
+ colorCard(c) {
157
+ const str = (0, poker_1.formatCard)(c);
158
+ if (c.suit === '♥' || c.suit === '♦' || c.rank === 'RJ') {
159
+ return chalk_1.default.red(str);
160
+ }
161
+ else {
162
+ return chalk_1.default.black.bgWhite(str);
163
+ }
164
+ }
165
+ printHand() {
166
+ console.log(chalk_1.default.blue('\n你的手牌:'));
167
+ console.log((0, view_1.renderCards)(this.hand));
168
+ console.log('\n');
169
+ }
170
+ handleTurn(action) {
171
+ return __awaiter(this, void 0, void 0, function* () {
172
+ if (action === 'bid') {
173
+ const { bid } = yield inquirer_1.default.prompt([{
174
+ type: 'confirm',
175
+ name: 'bid',
176
+ message: '你想叫地主吗?',
177
+ default: false
178
+ }]);
179
+ this.socket.emit('bid_landlord', bid ? 1 : 0);
180
+ }
181
+ else {
182
+ // Updated to use input for cards
183
+ const { cardInput } = yield inquirer_1.default.prompt([{
184
+ type: 'input',
185
+ name: 'cardInput',
186
+ message: '请输入要出的牌 (例如 "34567" 或 "3 4 5", 直接回车表示不出):'
187
+ }]);
188
+ const inputStr = (cardInput || '').trim();
189
+ if (!inputStr) {
190
+ const { confirmPass } = yield inquirer_1.default.prompt([{
191
+ type: 'confirm',
192
+ name: 'confirmPass',
193
+ message: '确认不出(PASS)吗?',
194
+ default: true
195
+ }]);
196
+ if (confirmPass) {
197
+ this.socket.emit('pass');
198
+ }
199
+ else {
200
+ return this.handleTurn('play');
201
+ }
202
+ }
203
+ else {
204
+ const selectedCards = this.parseCards(inputStr);
205
+ if (!selectedCards) {
206
+ console.log(chalk_1.default.red('无效的牌输入,请检查你的手牌和输入格式 (如 334455, 3 3 4 4)'));
207
+ return this.handleTurn('play');
208
+ }
209
+ this.socket.emit('play_cards', selectedCards);
210
+ }
211
+ }
212
+ this.myTurn = false;
213
+ });
214
+ }
215
+ // Helper to match input string to hand cards
216
+ // Supports "34567", "3344", "3 4 5", "10 J Q K"
217
+ parseCards(input) {
218
+ const tempHand = [...this.hand]; // Copy to track usage
219
+ const selected = [];
220
+ 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
+ let remaining = input.toUpperCase();
226
+ while (remaining.length > 0) {
227
+ // Skip separators
228
+ if (/^[\s,]+/.test(remaining)) {
229
+ remaining = remaining.replace(/^[\s,]+/, '');
230
+ continue;
231
+ }
232
+ let foundToken = '';
233
+ // 1. Check for specific multi-char tokens aliases first
234
+ if (remaining.startsWith('10')) {
235
+ foundToken = '10';
236
+ remaining = remaining.substring(2);
237
+ }
238
+ else if (remaining.startsWith('BJ')) {
239
+ foundToken = 'BJ';
240
+ remaining = remaining.substring(2);
241
+ }
242
+ else if (remaining.startsWith('RJ')) {
243
+ foundToken = 'RJ';
244
+ remaining = remaining.substring(2);
245
+ }
246
+ // 2. Check for Single char aliases for 10
247
+ else if (remaining.startsWith('0')) {
248
+ foundToken = '10';
249
+ remaining = remaining.substring(1);
250
+ }
251
+ else if (remaining.startsWith('T')) {
252
+ foundToken = '10';
253
+ remaining = remaining.substring(1);
254
+ }
255
+ // 3. Check for standard single chars
256
+ else {
257
+ const char = remaining[0];
258
+ if (poker_1.RANKS.includes(char)) {
259
+ foundToken = char;
260
+ remaining = remaining.substring(1);
261
+ }
262
+ else {
263
+ // Invalid character found
264
+ return null;
265
+ }
266
+ }
267
+ if (foundToken) {
268
+ tokens.push(foundToken);
269
+ }
270
+ }
271
+ // Now maps tokens to cards in hand
272
+ for (const token of tokens) {
273
+ // Token is already normalized rank string (e.g. '10', 'A', 'BJ')
274
+ const idx = tempHand.findIndex(c => c.rank === token);
275
+ if (idx === -1) {
276
+ // Not found in hand
277
+ return null;
278
+ }
279
+ // Remove from tempHand to avoid double counting same card
280
+ selected.push(tempHand[idx]);
281
+ tempHand.splice(idx, 1);
282
+ }
283
+ return selected;
284
+ }
285
+ }
286
+ exports.GameClient = GameClient;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeHand = analyzeHand;
4
+ exports.canBeat = canBeat;
5
+ function analyzeHand(cards) {
6
+ const len = cards.length;
7
+ if (len === 0)
8
+ return { type: 'invalid', rankValue: 0, length: 0 };
9
+ // Sort descending
10
+ 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 };
14
+ }
15
+ // Check Bomb (4 same rank)
16
+ if (len === 4 && cards[0].value === cards[3].value) {
17
+ return { type: 'bomb', rankValue: cards[0].value, length: 4 };
18
+ }
19
+ // Single
20
+ if (len === 1) {
21
+ return { type: 'single', rankValue: cards[0].value, length: 1 };
22
+ }
23
+ // Pair
24
+ if (len === 2 && cards[0].value === cards[1].value) {
25
+ return { type: 'pair', rankValue: cards[0].value, length: 2 };
26
+ }
27
+ // Triple
28
+ if (len === 3 && cards[0].value === cards[2].value) {
29
+ return { type: 'triple', rankValue: cards[0].value, length: 3 };
30
+ }
31
+ // Triple + One
32
+ 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
+ if (cards[0].value === cards[2].value) { // AAA B
36
+ return { type: 'triple_one', rankValue: cards[0].value, length: 4 };
37
+ }
38
+ if (cards[1].value === cards[3].value) { // B AAA
39
+ return { type: 'triple_one', rankValue: cards[1].value, length: 4 };
40
+ }
41
+ }
42
+ // Triple + Pair
43
+ if (len === 5) {
44
+ // AAA BB or BB AAA
45
+ if (cards[0].value === cards[2].value && cards[3].value === cards[4].value) { // AAA BB
46
+ return { type: 'triple_pair', rankValue: cards[0].value, length: 5 };
47
+ }
48
+ if (cards[0].value === cards[1].value && cards[2].value === cards[4].value) { // BB AAA
49
+ return { type: 'triple_pair', rankValue: cards[2].value, length: 5 };
50
+ }
51
+ }
52
+ // Straight (顺子): 5+ cards, consecutive values, max rank A (14). 2 (15) and Jokers (16,17) cannot be in straight.
53
+ 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 };
63
+ }
64
+ }
65
+ // Consecutive Pairs (连对): 3+ pairs (6+ cards), consecutive pair values. No 2, No Joker.
66
+ if (len >= 6 && len % 2 === 0) {
67
+ let isConsecutivePairs = true;
68
+ for (let i = 0; i < len; i += 2) {
69
+ // Check pair
70
+ if (cards[i].value !== cards[i + 1].value) {
71
+ isConsecutivePairs = false;
72
+ break;
73
+ }
74
+ // Check consecutive with previous pair
75
+ if (i > 0) {
76
+ if (cards[i].value !== cards[i - 2].value - 1) {
77
+ isConsecutivePairs = false;
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ if (isConsecutivePairs && cards[0].value < 15) {
83
+ return { type: 'consecutive_pairs', rankValue: cards[0].value, length: len };
84
+ }
85
+ }
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);
92
+ const triples = [];
93
+ for (const val in counts) {
94
+ if (counts[val] >= 3)
95
+ triples.push(Number(val));
96
+ }
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++;
103
+ }
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
+ }
111
+ return { type: 'invalid', rankValue: 0, length: 0 };
112
+ }
113
+ function canBeat(lastHand, newHand) {
114
+ const last = analyzeHand(lastHand);
115
+ const curr = analyzeHand(newHand);
116
+ if (curr.type === 'invalid')
117
+ return false;
118
+ // Rocket beats everything
119
+ if (curr.type === 'rocket')
120
+ return true;
121
+ if (last.type === 'rocket')
122
+ 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
+ }
129
+ if (last.type === 'bomb')
130
+ return false; // Current is not bomb/rocket
131
+ // Otherwise, types must match and lengths must match (usually)
132
+ if (curr.type !== last.type)
133
+ return false;
134
+ if (curr.length !== last.length)
135
+ return false;
136
+ return curr.rankValue > last.rankValue;
137
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RANKS = void 0;
4
+ exports.getCardValue = getCardValue;
5
+ exports.createDeck = createDeck;
6
+ exports.shuffleDeck = shuffleDeck;
7
+ exports.sortHand = sortHand;
8
+ exports.formatCard = formatCard;
9
+ exports.RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2', 'BJ', 'RJ'];
10
+ function getCardValue(rank) {
11
+ const index = exports.RANKS.indexOf(rank);
12
+ // 3 is index 0 -> value 3
13
+ // ...
14
+ return index + 3;
15
+ }
16
+ function createDeck() {
17
+ const suits = ['♠', '♥', '♣', '♦'];
18
+ // Normal cards 3 to 2
19
+ const normalRanks = exports.RANKS.slice(0, 13); // 3 to 2
20
+ let deck = [];
21
+ for (const r of normalRanks) {
22
+ for (const s of suits) {
23
+ deck.push({ suit: s, rank: r, value: getCardValue(r) });
24
+ }
25
+ }
26
+ // Jokers
27
+ deck.push({ suit: '', rank: 'BJ', value: 16 });
28
+ deck.push({ suit: '', rank: 'RJ', value: 17 });
29
+ return deck;
30
+ }
31
+ function shuffleDeck(deck) {
32
+ for (let i = deck.length - 1; i > 0; i--) {
33
+ const j = Math.floor(Math.random() * (i + 1));
34
+ [deck[i], deck[j]] = [deck[j], deck[i]];
35
+ }
36
+ return deck;
37
+ }
38
+ function sortHand(hand) {
39
+ // Sort descending by value
40
+ return hand.sort((a, b) => b.value - a.value);
41
+ }
42
+ function formatCard(c) {
43
+ // Add colors later via chalk in client, here just string
44
+ if (c.rank === 'RJ')
45
+ return ' [ 大王 ] '; // Big Joker
46
+ if (c.rank === 'BJ')
47
+ return ' [ 小王 ] '; // Little Joker
48
+ // Pad rank with space if single digit for alignment
49
+ const r = c.rank === '10' ? '10' : ` ${c.rank}`;
50
+ return ` [${c.suit}${r}] `;
51
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderCards = renderCards;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ function renderCards(cards) {
9
+ if (cards.length === 0)
10
+ return '';
11
+ let line1 = '';
12
+ let line2 = '';
13
+ let line3 = '';
14
+ let line4 = '';
15
+ const colorMap = (c, str) => {
16
+ if (c.suit === '♥' || c.suit === '♦' || c.rank === 'RJ') {
17
+ return chalk_1.default.red(str);
18
+ }
19
+ else if (c.suit === '♠' || c.suit === '♣' || c.rank === 'BJ') {
20
+ return chalk_1.default.white(str); // Spades/Clubs white on black terminal usually best, or black on white
21
+ }
22
+ return str;
23
+ };
24
+ cards.forEach((card, i) => {
25
+ const isFirst = i === 0;
26
+ // Rank Display (2 chars)
27
+ let rankStr = card.rank;
28
+ if (card.rank === '10')
29
+ rankStr = '10';
30
+ else if (card.rank === 'BJ')
31
+ rankStr = 'BJ';
32
+ else if (card.rank === 'RJ')
33
+ rankStr = 'RJ';
34
+ else
35
+ rankStr = `${card.rank} `; // Pad single digit
36
+ // Suit Display (2 chars)
37
+ let suitStr = card.suit ? `${card.suit} ` : ' ';
38
+ // Apply colors to inner content
39
+ const coloredRank = colorMap(card, rankStr);
40
+ const coloredSuit = colorMap(card, suitStr);
41
+ // Build segments
42
+ // Top: ┌──┐ or ──┐
43
+ // Mid1: │RR│ or RR│
44
+ // Mid2: │SS│ or SS│
45
+ // Bot: └──┘ or ──┘
46
+ if (isFirst) {
47
+ line1 += `┌──┐`;
48
+ line2 += `│${coloredRank}│`;
49
+ line3 += `│${coloredSuit}│`;
50
+ line4 += `└──┘`;
51
+ }
52
+ else {
53
+ line1 += `──┐`;
54
+ line2 += `${coloredRank}│`;
55
+ line3 += `${coloredSuit}│`;
56
+ line4 += `──┘`;
57
+ }
58
+ });
59
+ return `${line1}\n${line2}\n${line3}\n${line4}`;
60
+ }
package/dist/index.js ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
+ return new (P || (P = Promise))(function (resolve, reject) {
6
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
10
+ });
11
+ };
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ const inquirer_1 = __importDefault(require("inquirer"));
17
+ const chalk_1 = __importDefault(require("chalk"));
18
+ const figlet_1 = __importDefault(require("figlet"));
19
+ const server_1 = require("./server");
20
+ const client_1 = require("./client");
21
+ const os_1 = __importDefault(require("os"));
22
+ console.clear();
23
+ console.log(chalk_1.default.red(figlet_1.default.textSync('Maid Poker', { font: 'Standard' })));
24
+ function getLocalIPs() {
25
+ const nets = os_1.default.networkInterfaces();
26
+ const results = [];
27
+ for (const name of Object.keys(nets)) {
28
+ for (const net of nets[name]) {
29
+ // Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
30
+ if (net.family === 'IPv4' && !net.internal) {
31
+ results.push(net.address);
32
+ }
33
+ }
34
+ }
35
+ return results.length > 0 ? results : ['127.0.0.1'];
36
+ }
37
+ function main() {
38
+ return __awaiter(this, void 0, void 0, function* () {
39
+ const { mode } = yield inquirer_1.default.prompt([
40
+ {
41
+ type: 'list',
42
+ name: 'mode',
43
+ message: '你想创建游戏(Host)还是加入游戏(Join)?',
44
+ choices: ['创建游戏 (Host Game)', '加入游戏 (Join Game)']
45
+ }
46
+ ]);
47
+ if (mode.includes('Host')) {
48
+ const ips = getLocalIPs();
49
+ console.log(chalk_1.default.blue(`\n服务器已启动,请尝试以下IP: \n${ips.map(ip => ` - ${ip}`).join('\n')}`));
50
+ // Start the server
51
+ new server_1.GameServer();
52
+ const { joinAsPlayer } = yield inquirer_1.default.prompt([{
53
+ type: 'confirm',
54
+ name: 'joinAsPlayer',
55
+ message: '你想在本机参与游戏吗?',
56
+ default: true
57
+ }]);
58
+ if (joinAsPlayer) {
59
+ const { name } = yield inquirer_1.default.prompt([{
60
+ type: 'input',
61
+ name: 'name',
62
+ message: '请输入你的昵称:',
63
+ default: '房主'
64
+ }]);
65
+ new client_1.GameClient('http://localhost:3000', name);
66
+ }
67
+ }
68
+ else {
69
+ const { ip, name } = yield inquirer_1.default.prompt([
70
+ {
71
+ type: 'input',
72
+ name: 'ip',
73
+ message: '请输入主机IP地址:',
74
+ default: '127.0.0.1'
75
+ },
76
+ {
77
+ type: 'input',
78
+ name: 'name',
79
+ message: '请输入你的昵称:',
80
+ }
81
+ ]);
82
+ new client_1.GameClient(`http://${ip}:3000`, name);
83
+ }
84
+ });
85
+ }
86
+ main().catch(console.error);
package/dist/server.js ADDED
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GameServer = void 0;
4
+ const poker_1 = require("./game/poker");
5
+ const socket_io_1 = require("socket.io");
6
+ const http_1 = require("http");
7
+ const PORT = 3000;
8
+ class GameServer {
9
+ constructor() {
10
+ this.players = [];
11
+ this.state = {
12
+ phase: 'waiting',
13
+ deck: [],
14
+ currentTurnIndex: 0,
15
+ landlordIndex: -1,
16
+ lastPlayedCards: [],
17
+ lastPlayedIndex: -1
18
+ };
19
+ const httpServer = (0, http_1.createServer)();
20
+ this.io = new socket_io_1.Server(httpServer);
21
+ this.io.on('connection', (socket) => {
22
+ socket.on('join_game', (name) => this.handleJoin(socket, name));
23
+ socket.on('disconnect', () => this.handleDisconnect(socket));
24
+ socket.on('player_ready', () => this.handleReady(socket));
25
+ // Game events
26
+ socket.on('bid_landlord', (bid) => this.handleBid(socket, bid));
27
+ socket.on('play_cards', (cards) => this.handlePlay(socket, cards));
28
+ socket.on('pass', () => this.handlePass(socket));
29
+ });
30
+ httpServer.listen(PORT, '0.0.0.0', () => {
31
+ // Logs handled in index.ts usually, but good to have here
32
+ });
33
+ }
34
+ handleJoin(socket, name) {
35
+ if (this.players.length >= 3) {
36
+ socket.emit('error_msg', '房间已满');
37
+ return;
38
+ }
39
+ // Allow joining if in waiting phase
40
+ if (this.state.phase !== 'waiting' && this.state.phase !== 'ended') {
41
+ socket.emit('error_msg', '游戏进行中');
42
+ return;
43
+ }
44
+ const newPlayer = {
45
+ id: socket.id,
46
+ socket,
47
+ name,
48
+ hand: [],
49
+ role: 'peasant',
50
+ isReady: false
51
+ };
52
+ this.players.push(newPlayer);
53
+ console.log(`${name} 加入了游戏. (${this.players.length}/3)`);
54
+ // Broadcast updated player list
55
+ this.broadcastPlayerList();
56
+ // If 3 players, ask them to get ready (or auto-ready for first game?)
57
+ // For UX consistency, let's treat "Join" as implicitly ready for the first game if we want instant start,
58
+ // BUT for "Restart" feature, explicit ready is better.
59
+ // Let's make the first game auto-start when 3 join for simplicity,
60
+ // and subsequent games require explicit ready.
61
+ if (this.players.length === 3) {
62
+ console.log('房间已满,准备开始...');
63
+ // Mark all valid players as ready for the first game?
64
+ // Or just start immediately.
65
+ this.startGame();
66
+ }
67
+ }
68
+ handleReady(socket) {
69
+ const player = this.players.find(p => p.id === socket.id);
70
+ if (!player)
71
+ return;
72
+ player.isReady = true;
73
+ console.log(`${player.name} 已准备.`);
74
+ // Notify others?
75
+ // Check if all 3 are ready
76
+ if (this.players.length === 3 && this.players.every(p => p.isReady)) {
77
+ this.startGame();
78
+ }
79
+ }
80
+ handleDisconnect(socket) {
81
+ const player = this.players.find(p => p.id === socket.id);
82
+ this.players = this.players.filter(p => p.id !== socket.id);
83
+ if (player) {
84
+ console.log(`客户端断开连接: ${player.name} (${socket.id})`);
85
+ }
86
+ this.broadcastPlayerList();
87
+ // Reset game if someone leaves during play
88
+ if (this.state.phase !== 'waiting' && this.state.phase !== 'ended') {
89
+ this.io.emit('error_msg', '玩家断开连接,游戏重置。');
90
+ this.resetGame();
91
+ }
92
+ }
93
+ broadcastPlayerList() {
94
+ const names = this.players.map(p => p.name);
95
+ this.io.emit('player_list_update', names);
96
+ }
97
+ startGame() {
98
+ console.log('所有玩家准备就绪,游戏开始!');
99
+ this.state.phase = 'bidding';
100
+ this.state.lastPlayedCards = [];
101
+ this.state.lastPlayedIndex = -1;
102
+ this.state.landlordIndex = -1;
103
+ this.state.deck = [];
104
+ // Reset ready status for next time? No, do it at end.
105
+ this.players.forEach(p => {
106
+ p.isReady = false;
107
+ p.role = 'peasant';
108
+ p.hand = [];
109
+ });
110
+ // 1. Shuffle and Deal
111
+ const fullDeck = (0, poker_1.shuffleDeck)((0, poker_1.createDeck)());
112
+ this.state.deck = fullDeck.splice(0, 3); // 3 hidden cards
113
+ // Deal 17 cards to each
114
+ this.players[0].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 17));
115
+ this.players[1].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 17));
116
+ this.players[2].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 17));
117
+ this.players.forEach(p => {
118
+ p.socket.emit('game_start', { hand: p.hand });
119
+ });
120
+ // 2. Start Bidding (Simple random start for now)
121
+ this.state.currentTurnIndex = Math.floor(Math.random() * 3);
122
+ this.notifyTurn('bid');
123
+ }
124
+ notifyTurn(action) {
125
+ const player = this.players[this.state.currentTurnIndex];
126
+ this.io.emit('turn_update', {
127
+ playerIndex: this.state.currentTurnIndex,
128
+ playerName: player.name,
129
+ action
130
+ });
131
+ player.socket.emit('your_turn', { action });
132
+ }
133
+ // Placeholder handlers
134
+ handleBid(socket, bid) {
135
+ const playerIndex = this.players.findIndex(p => p.id === socket.id);
136
+ if (playerIndex !== this.state.currentTurnIndex)
137
+ return;
138
+ if (bid > 0) {
139
+ // Claimed!
140
+ this.state.landlordIndex = playerIndex;
141
+ this.players[playerIndex].role = 'landlord';
142
+ this.players[playerIndex].hand.push(...this.state.deck);
143
+ this.players[playerIndex].hand = (0, poker_1.sortHand)(this.players[playerIndex].hand);
144
+ this.state.phase = 'playing';
145
+ this.io.emit('landlord_chosen', {
146
+ landlordName: this.players[playerIndex].name,
147
+ hiddenCards: this.state.deck
148
+ });
149
+ // Sync new hand
150
+ this.players[playerIndex].socket.emit('hand_update', this.players[playerIndex].hand);
151
+ // Landlord plays first
152
+ this.notifyTurn('play');
153
+ }
154
+ else {
155
+ // Pass bid
156
+ this.state.currentTurnIndex = (this.state.currentTurnIndex + 1) % 3;
157
+ // MVP: Infinite loop until someone takes it
158
+ this.notifyTurn('bid');
159
+ }
160
+ }
161
+ handlePlay(socket, cards) {
162
+ const playerIndex = this.players.findIndex(p => p.id === socket.id);
163
+ if (playerIndex !== this.state.currentTurnIndex)
164
+ return;
165
+ // Validate Play
166
+ const { canBeat, analyzeHand } = require('./game/logic');
167
+ // If this is a new round (lastPlayedCards is empty), just check validity of the hand itself
168
+ if (this.state.lastPlayedCards.length === 0) {
169
+ const analysis = analyzeHand(cards);
170
+ if (analysis.type === 'invalid') {
171
+ socket.emit('error_msg', '无效的牌型!');
172
+ this.players[playerIndex].socket.emit('your_turn', { action: 'play' });
173
+ return;
174
+ }
175
+ }
176
+ else {
177
+ // Check if it beats the last played cards
178
+ const valid = canBeat(this.state.lastPlayedCards, cards);
179
+ if (!valid) {
180
+ socket.emit('error_msg', '出牌无效: 你的牌必须大过上家!');
181
+ this.players[playerIndex].socket.emit('your_turn', { action: 'play' });
182
+ return;
183
+ }
184
+ }
185
+ this.state.lastPlayedCards = cards;
186
+ this.state.lastPlayedIndex = playerIndex;
187
+ // Remove cards from hand
188
+ const p = this.players[playerIndex];
189
+ cards.forEach(c => {
190
+ const idx = p.hand.findIndex(h => h.suit === c.suit && h.rank === c.rank);
191
+ if (idx !== -1)
192
+ p.hand.splice(idx, 1);
193
+ });
194
+ p.socket.emit('hand_update', p.hand);
195
+ this.io.emit('player_played', {
196
+ playerName: p.name,
197
+ cards: cards,
198
+ remainingCount: p.hand.length
199
+ });
200
+ if (p.hand.length === 0) {
201
+ this.state.phase = 'ended';
202
+ this.io.emit('game_over', { winner: p.name, role: p.role });
203
+ // Don't kill server, wait for ready
204
+ }
205
+ else {
206
+ this.state.currentTurnIndex = (this.state.currentTurnIndex + 1) % 3;
207
+ this.notifyTurn('play');
208
+ }
209
+ }
210
+ handlePass(socket) {
211
+ const playerIndex = this.players.findIndex(p => p.id === socket.id);
212
+ if (playerIndex !== this.state.currentTurnIndex)
213
+ return;
214
+ if (this.state.lastPlayedIndex === playerIndex || this.state.lastPlayedIndex === -1) {
215
+ // Cannot pass if you started the round
216
+ socket.emit('error_msg', '这是新回合,必须出牌');
217
+ this.players[playerIndex].socket.emit('your_turn', { action: 'play' });
218
+ return; // Ask them to play again
219
+ }
220
+ this.io.emit('player_passed', { playerName: this.players[playerIndex].name });
221
+ this.state.currentTurnIndex = (this.state.currentTurnIndex + 1) % 3;
222
+ // If next player is the one who played last, they win the round and start new
223
+ if (this.state.currentTurnIndex === this.state.lastPlayedIndex) {
224
+ this.state.lastPlayedCards = []; // Clear
225
+ this.io.emit('new_round', { playerName: this.players[this.state.currentTurnIndex].name });
226
+ }
227
+ this.notifyTurn('play');
228
+ }
229
+ resetGame() {
230
+ this.state.phase = 'waiting';
231
+ this.players.forEach(p => {
232
+ p.hand = [];
233
+ p.role = 'peasant';
234
+ p.isReady = false;
235
+ });
236
+ this.state.deck = [];
237
+ this.broadcastPlayerList();
238
+ }
239
+ }
240
+ exports.GameServer = GameServer;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "maid-poker-cli",
3
+ "version": "1.0.0",
4
+ "description": "女仆扑克牌 (Maid Poker) - A Command Line Dou Dizhu Game",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "maid-poker": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "start": "ts-node src/index.ts",
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "game",
19
+ "poker",
20
+ "dou-dizhu",
21
+ "cli",
22
+ "multiplayer"
23
+ ],
24
+ "author": "Joyeka",
25
+ "license": "ISC",
26
+ "dependencies": {
27
+ "@types/figlet": "^1.7.0",
28
+ "@types/inquirer": "^9.0.9",
29
+ "@types/node": "^25.0.3",
30
+ "chalk": "^4.1.2",
31
+ "figlet": "^1.9.4",
32
+ "inquirer": "^8.2.5",
33
+ "socket.io": "^4.8.3",
34
+ "socket.io-client": "^4.8.3",
35
+ "ts-node": "^10.9.2",
36
+ "typescript": "^5.7.3"
37
+ }
38
+ }