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 +81 -5
- package/dist/client.js +72 -76
- package/dist/game/__tests__/logic.test.js +467 -0
- package/dist/game/logic.js +217 -60
- package/dist/game/poker.js +19 -0
- package/dist/index.js +33 -2
- package/dist/server.js +69 -61
- package/package.json +9 -1
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
|
-
|
|
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 >=
|
|
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
|
-
|
|
54
|
-
|
|
62
|
+
const modeText = this.state.gameMode === 'three_player' ? '三人斗地主' : '四人斗地主';
|
|
63
|
+
console.log(`${name} 加入了游戏. (${this.players.length}/${this.state.requiredPlayers}) [${modeText}]`);
|
|
55
64
|
this.broadcastPlayerList();
|
|
56
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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', {
|
|
131
|
+
p.socket.emit('game_start', {
|
|
132
|
+
hand: p.hand,
|
|
133
|
+
gameMode: this.state.gameMode
|
|
134
|
+
});
|
|
119
135
|
});
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 &&
|
|
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) %
|
|
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;
|
|
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) %
|
|
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 = [];
|
|
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.
|
|
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
|
}
|