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/dist/server.js CHANGED
@@ -6,19 +6,28 @@ const socket_io_1 = require("socket.io");
6
6
  const http_1 = require("http");
7
7
  const PORT = 3000;
8
8
  class GameServer {
9
- constructor() {
9
+ constructor(gameMode = 'three_player') {
10
10
  this.players = [];
11
11
  this.state = {
12
12
  phase: 'waiting',
13
+ gameMode: 'three_player',
13
14
  deck: [],
14
15
  currentTurnIndex: 0,
15
16
  landlordIndex: -1,
16
17
  lastPlayedCards: [],
17
- lastPlayedIndex: -1
18
+ lastPlayedIndex: -1,
19
+ requiredPlayers: 3
18
20
  };
21
+ this.state.gameMode = gameMode;
22
+ this.state.requiredPlayers = gameMode === 'three_player' ? 3 : 4;
19
23
  const httpServer = (0, http_1.createServer)();
20
24
  this.io = new socket_io_1.Server(httpServer);
21
25
  this.io.on('connection', (socket) => {
26
+ // 发送当前游戏模式给客户端
27
+ socket.emit('game_mode', {
28
+ mode: this.state.gameMode,
29
+ requiredPlayers: this.state.requiredPlayers
30
+ });
22
31
  socket.on('join_game', (name) => this.handleJoin(socket, name));
23
32
  socket.on('disconnect', () => this.handleDisconnect(socket));
24
33
  socket.on('player_ready', () => this.handleReady(socket));
@@ -28,15 +37,15 @@ class GameServer {
28
37
  socket.on('pass', () => this.handlePass(socket));
29
38
  });
30
39
  httpServer.listen(PORT, '0.0.0.0', () => {
31
- // Logs handled in index.ts usually, but good to have here
40
+ const modeText = gameMode === 'three_player' ? '三人斗地主' : '四人斗地主(2副牌)';
41
+ console.log(`游戏模式: ${modeText}`);
32
42
  });
33
43
  }
34
44
  handleJoin(socket, name) {
35
- if (this.players.length >= 3) {
45
+ if (this.players.length >= this.state.requiredPlayers) {
36
46
  socket.emit('error_msg', '房间已满');
37
47
  return;
38
48
  }
39
- // Allow joining if in waiting phase
40
49
  if (this.state.phase !== 'waiting' && this.state.phase !== 'ended') {
41
50
  socket.emit('error_msg', '游戏进行中');
42
51
  return;
@@ -50,18 +59,11 @@ class GameServer {
50
59
  isReady: false
51
60
  };
52
61
  this.players.push(newPlayer);
53
- console.log(`${name} 加入了游戏. (${this.players.length}/3)`);
54
- // Broadcast updated player list
62
+ const modeText = this.state.gameMode === 'three_player' ? '三人斗地主' : '四人斗地主';
63
+ console.log(`${name} 加入了游戏. (${this.players.length}/${this.state.requiredPlayers}) [${modeText}]`);
55
64
  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) {
65
+ if (this.players.length === this.state.requiredPlayers) {
62
66
  console.log('房间已满,准备开始...');
63
- // Mark all valid players as ready for the first game?
64
- // Or just start immediately.
65
67
  this.startGame();
66
68
  }
67
69
  }
@@ -71,9 +73,7 @@ class GameServer {
71
73
  return;
72
74
  player.isReady = true;
73
75
  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)) {
76
+ if (this.players.length === this.state.requiredPlayers && this.players.every(p => p.isReady)) {
77
77
  this.startGame();
78
78
  }
79
79
  }
@@ -84,7 +84,6 @@ class GameServer {
84
84
  console.log(`客户端断开连接: ${player.name} (${socket.id})`);
85
85
  }
86
86
  this.broadcastPlayerList();
87
- // Reset game if someone leaves during play
88
87
  if (this.state.phase !== 'waiting' && this.state.phase !== 'ended') {
89
88
  this.io.emit('error_msg', '玩家断开连接,游戏重置。');
90
89
  this.resetGame();
@@ -92,33 +91,49 @@ class GameServer {
92
91
  }
93
92
  broadcastPlayerList() {
94
93
  const names = this.players.map(p => p.name);
95
- this.io.emit('player_list_update', names);
94
+ this.io.emit('player_list_update', {
95
+ names,
96
+ required: this.state.requiredPlayers,
97
+ gameMode: this.state.gameMode
98
+ });
96
99
  }
97
100
  startGame() {
98
- console.log('所有玩家准备就绪,游戏开始!');
101
+ const modeText = this.state.gameMode === 'three_player' ? '三人斗地主' : '四人斗地主';
102
+ console.log(`所有玩家准备就绪,${modeText}开始!`);
99
103
  this.state.phase = 'bidding';
100
104
  this.state.lastPlayedCards = [];
101
105
  this.state.lastPlayedIndex = -1;
102
106
  this.state.landlordIndex = -1;
103
107
  this.state.deck = [];
104
- // Reset ready status for next time? No, do it at end.
105
108
  this.players.forEach(p => {
106
109
  p.isReady = false;
107
110
  p.role = 'peasant';
108
111
  p.hand = [];
109
112
  });
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));
113
+ if (this.state.gameMode === 'three_player') {
114
+ // 三人斗地主:1副牌54张,每人17张,底牌3张
115
+ const fullDeck = (0, poker_1.shuffleDeck)((0, poker_1.createDeck)());
116
+ this.state.deck = fullDeck.splice(0, 3);
117
+ this.players[0].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 17));
118
+ this.players[1].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 17));
119
+ this.players[2].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 17));
120
+ }
121
+ else {
122
+ // 四人斗地主:2副牌108张,每人25张,底牌8张
123
+ const fullDeck = (0, poker_1.shuffleDeck)((0, poker_1.createDoubleDeck)());
124
+ this.state.deck = fullDeck.splice(0, 8);
125
+ this.players[0].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 25));
126
+ this.players[1].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 25));
127
+ this.players[2].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 25));
128
+ this.players[3].hand = (0, poker_1.sortHand)(fullDeck.splice(0, 25));
129
+ }
117
130
  this.players.forEach(p => {
118
- p.socket.emit('game_start', { hand: p.hand });
131
+ p.socket.emit('game_start', {
132
+ hand: p.hand,
133
+ gameMode: this.state.gameMode
134
+ });
119
135
  });
120
- // 2. Start Bidding (Simple random start for now)
121
- this.state.currentTurnIndex = Math.floor(Math.random() * 3);
136
+ this.state.currentTurnIndex = Math.floor(Math.random() * this.state.requiredPlayers);
122
137
  this.notifyTurn('bid');
123
138
  }
124
139
  notifyTurn(action) {
@@ -126,17 +141,16 @@ class GameServer {
126
141
  this.io.emit('turn_update', {
127
142
  playerIndex: this.state.currentTurnIndex,
128
143
  playerName: player.name,
129
- action
144
+ action,
145
+ gameMode: this.state.gameMode
130
146
  });
131
- player.socket.emit('your_turn', { action });
147
+ player.socket.emit('your_turn', { action, gameMode: this.state.gameMode });
132
148
  }
133
- // Placeholder handlers
134
149
  handleBid(socket, bid) {
135
150
  const playerIndex = this.players.findIndex(p => p.id === socket.id);
136
151
  if (playerIndex !== this.state.currentTurnIndex)
137
152
  return;
138
153
  if (bid > 0) {
139
- // Claimed!
140
154
  this.state.landlordIndex = playerIndex;
141
155
  this.players[playerIndex].role = 'landlord';
142
156
  this.players[playerIndex].hand.push(...this.state.deck);
@@ -144,17 +158,14 @@ class GameServer {
144
158
  this.state.phase = 'playing';
145
159
  this.io.emit('landlord_chosen', {
146
160
  landlordName: this.players[playerIndex].name,
147
- hiddenCards: this.state.deck
161
+ hiddenCards: this.state.deck,
162
+ gameMode: this.state.gameMode
148
163
  });
149
- // Sync new hand
150
164
  this.players[playerIndex].socket.emit('hand_update', this.players[playerIndex].hand);
151
- // Landlord plays first
152
165
  this.notifyTurn('play');
153
166
  }
154
167
  else {
155
- // Pass bid
156
- this.state.currentTurnIndex = (this.state.currentTurnIndex + 1) % 3;
157
- // MVP: Infinite loop until someone takes it
168
+ this.state.currentTurnIndex = (this.state.currentTurnIndex + 1) % this.state.requiredPlayers;
158
169
  this.notifyTurn('bid');
159
170
  }
160
171
  }
@@ -162,32 +173,31 @@ class GameServer {
162
173
  const playerIndex = this.players.findIndex(p => p.id === socket.id);
163
174
  if (playerIndex !== this.state.currentTurnIndex)
164
175
  return;
165
- // Validate Play
166
176
  const { canBeat, analyzeHand } = require('./game/logic');
167
- // If this is a new round (lastPlayedCards is empty), just check validity of the hand itself
168
177
  if (this.state.lastPlayedCards.length === 0) {
169
- const analysis = analyzeHand(cards);
178
+ const analysis = analyzeHand(cards, this.state.gameMode);
170
179
  if (analysis.type === 'invalid') {
171
180
  socket.emit('error_msg', '无效的牌型!');
172
- this.players[playerIndex].socket.emit('your_turn', { action: 'play' });
181
+ this.players[playerIndex].socket.emit('your_turn', { action: 'play', gameMode: this.state.gameMode });
173
182
  return;
174
183
  }
175
184
  }
176
185
  else {
177
- // Check if it beats the last played cards
178
- const valid = canBeat(this.state.lastPlayedCards, cards);
186
+ const valid = canBeat(this.state.lastPlayedCards, cards, this.state.gameMode);
179
187
  if (!valid) {
180
188
  socket.emit('error_msg', '出牌无效: 你的牌必须大过上家!');
181
- this.players[playerIndex].socket.emit('your_turn', { action: 'play' });
189
+ this.players[playerIndex].socket.emit('your_turn', { action: 'play', gameMode: this.state.gameMode });
182
190
  return;
183
191
  }
184
192
  }
185
193
  this.state.lastPlayedCards = cards;
186
194
  this.state.lastPlayedIndex = playerIndex;
187
- // Remove cards from hand
188
195
  const p = this.players[playerIndex];
196
+ // 四人模式需要考虑两副牌中同值同花的牌,使用 deckIndex 区分
189
197
  cards.forEach(c => {
190
- const idx = p.hand.findIndex(h => h.suit === c.suit && h.rank === c.rank);
198
+ const idx = p.hand.findIndex(h => h.suit === c.suit &&
199
+ h.rank === c.rank &&
200
+ (this.state.gameMode === 'three_player' || h.deckIndex === c.deckIndex));
191
201
  if (idx !== -1)
192
202
  p.hand.splice(idx, 1);
193
203
  });
@@ -195,15 +205,15 @@ class GameServer {
195
205
  this.io.emit('player_played', {
196
206
  playerName: p.name,
197
207
  cards: cards,
198
- remainingCount: p.hand.length
208
+ remainingCount: p.hand.length,
209
+ gameMode: this.state.gameMode
199
210
  });
200
211
  if (p.hand.length === 0) {
201
212
  this.state.phase = 'ended';
202
- this.io.emit('game_over', { winner: p.name, role: p.role });
203
- // Don't kill server, wait for ready
213
+ this.io.emit('game_over', { winner: p.name, role: p.role, gameMode: this.state.gameMode });
204
214
  }
205
215
  else {
206
- this.state.currentTurnIndex = (this.state.currentTurnIndex + 1) % 3;
216
+ this.state.currentTurnIndex = (this.state.currentTurnIndex + 1) % this.state.requiredPlayers;
207
217
  this.notifyTurn('play');
208
218
  }
209
219
  }
@@ -212,16 +222,14 @@ class GameServer {
212
222
  if (playerIndex !== this.state.currentTurnIndex)
213
223
  return;
214
224
  if (this.state.lastPlayedIndex === playerIndex || this.state.lastPlayedIndex === -1) {
215
- // Cannot pass if you started the round
216
225
  socket.emit('error_msg', '这是新回合,必须出牌');
217
- this.players[playerIndex].socket.emit('your_turn', { action: 'play' });
218
- return; // Ask them to play again
226
+ this.players[playerIndex].socket.emit('your_turn', { action: 'play', gameMode: this.state.gameMode });
227
+ return;
219
228
  }
220
229
  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
230
+ this.state.currentTurnIndex = (this.state.currentTurnIndex + 1) % this.state.requiredPlayers;
223
231
  if (this.state.currentTurnIndex === this.state.lastPlayedIndex) {
224
- this.state.lastPlayedCards = []; // Clear
232
+ this.state.lastPlayedCards = [];
225
233
  this.io.emit('new_round', { playerName: this.players[this.state.currentTurnIndex].name });
226
234
  }
227
235
  this.notifyTurn('play');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maid-poker-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "女仆扑克牌 (Maid Poker) - A Command Line Dou Dizhu Game",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -12,6 +12,9 @@
12
12
  "scripts": {
13
13
  "start": "ts-node src/index.ts",
14
14
  "build": "tsc",
15
+ "test": "jest",
16
+ "test:watch": "jest --watch",
17
+ "test:coverage": "jest --coverage",
15
18
  "prepublishOnly": "npm run build"
16
19
  },
17
20
  "keywords": [
@@ -34,5 +37,10 @@
34
37
  "socket.io-client": "^4.8.3",
35
38
  "ts-node": "^10.9.2",
36
39
  "typescript": "^5.7.3"
40
+ },
41
+ "devDependencies": {
42
+ "@types/jest": "^29.5.14",
43
+ "jest": "^29.7.0",
44
+ "ts-jest": "^29.4.6"
37
45
  }
38
46
  }