most-box 0.1.3 → 0.1.4
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/out/404/index.html +2 -2
- package/out/404.html +2 -2
- package/out/__next.__PAGE__.txt +5 -5
- package/out/__next._full.txt +13 -13
- package/out/__next._head.txt +3 -3
- package/out/__next._index.txt +6 -6
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/chunks/02zzxfop_k6tl.css +1 -0
- package/out/_next/static/chunks/03gsbg0fr00ey.js +1 -0
- package/out/_next/static/chunks/06lttvu7563zo.css +1 -0
- package/out/_next/static/chunks/0fo9-h4knidcz.js +1 -0
- package/out/_next/static/chunks/0hyds~bp.auvh.js +1 -0
- package/out/_next/static/chunks/0i9sfdypwuw8~.js +1 -0
- package/out/_next/static/chunks/{0_0oph_z1az14.js → 0puk.7e.tr2zy.js} +1 -1
- package/out/_next/static/chunks/0s1k6rlwy02c2.js +1 -0
- package/out/_next/static/chunks/0sgltmtk_9s8p.css +1 -0
- package/out/_next/static/chunks/0snehvtvu1n4q.js +1 -0
- package/out/_next/static/chunks/{12nr19.nnn6s3.js → 0t_3xxx4zkerp.js} +2 -2
- package/out/_next/static/chunks/0u38kke9vhobe.js +1 -0
- package/out/_next/static/chunks/0vd4_a5x-wpdh.js +1 -0
- package/out/_next/static/chunks/0xx_10jns1.s7.css +1 -0
- package/out/_next/static/chunks/{0m_5nb6x8qy._.js → 10f-t2n4y1zx8.js} +1 -1
- package/out/_next/static/chunks/13jdyag9a-~kk.js +1 -0
- package/out/_not-found/__next._full.txt +11 -11
- package/out/_not-found/__next._head.txt +3 -3
- package/out/_not-found/__next._index.txt +6 -6
- package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +1 -1
- package/out/_not-found/index.html +2 -2
- package/out/_not-found/index.txt +11 -11
- package/out/admin/__next._full.txt +12 -12
- package/out/admin/__next._head.txt +3 -3
- package/out/admin/__next._index.txt +6 -6
- package/out/admin/__next._tree.txt +1 -1
- package/out/admin/__next.admin.__PAGE__.txt +4 -4
- package/out/admin/__next.admin.txt +3 -3
- package/out/admin/index.html +2 -2
- package/out/admin/index.txt +12 -12
- package/out/app/__next._full.txt +12 -12
- package/out/app/__next._head.txt +3 -3
- package/out/app/__next._index.txt +6 -6
- package/out/app/__next._tree.txt +1 -1
- package/out/app/__next.app.__PAGE__.txt +4 -4
- package/out/app/__next.app.txt +3 -3
- package/out/app/index.html +2 -2
- package/out/app/index.txt +12 -12
- package/out/chat/__next._full.txt +12 -12
- package/out/chat/__next._head.txt +3 -3
- package/out/chat/__next._index.txt +6 -6
- package/out/chat/__next._tree.txt +1 -1
- package/out/chat/__next.chat.__PAGE__.txt +4 -4
- package/out/chat/__next.chat.txt +3 -3
- package/out/chat/index.html +2 -2
- package/out/chat/index.txt +12 -12
- package/out/chat/join/__next._full.txt +12 -12
- package/out/chat/join/__next._head.txt +3 -3
- package/out/chat/join/__next._index.txt +6 -6
- package/out/chat/join/__next._tree.txt +1 -1
- package/out/chat/join/__next.chat.join.__PAGE__.txt +4 -4
- package/out/chat/join/__next.chat.join.txt +3 -3
- package/out/chat/join/__next.chat.txt +3 -3
- package/out/chat/join/index.html +2 -2
- package/out/chat/join/index.txt +12 -12
- package/out/download/__next._full.txt +16 -16
- package/out/download/__next._head.txt +3 -3
- package/out/download/__next._index.txt +6 -6
- package/out/download/__next._tree.txt +2 -2
- package/out/download/__next.download.__PAGE__.txt +6 -6
- package/out/download/__next.download.txt +3 -3
- package/out/download/index.html +2 -2
- package/out/download/index.txt +16 -16
- package/out/game/__next._full.txt +20 -0
- package/out/game/__next._head.txt +5 -0
- package/out/{gandengyan → game}/__next._index.txt +6 -6
- package/out/game/__next._tree.txt +5 -0
- package/out/game/__next.game.__PAGE__.txt +6 -0
- package/out/game/__next.game.txt +5 -0
- package/out/game/gandengyan/__next._full.txt +26 -0
- package/out/game/gandengyan/__next._head.txt +5 -0
- package/out/game/gandengyan/__next._index.txt +9 -0
- package/out/game/gandengyan/__next._tree.txt +6 -0
- package/out/game/gandengyan/__next.game.gandengyan.__PAGE__.txt +10 -0
- package/out/game/gandengyan/__next.game.gandengyan.txt +5 -0
- package/out/game/gandengyan/__next.game.txt +5 -0
- package/out/game/gandengyan/index.html +15 -0
- package/out/game/gandengyan/index.txt +26 -0
- package/out/game/index.html +1 -0
- package/out/game/index.txt +20 -0
- package/out/game/zhajinhua/__next._full.txt +25 -0
- package/out/game/zhajinhua/__next._head.txt +5 -0
- package/out/game/zhajinhua/__next._index.txt +9 -0
- package/out/game/zhajinhua/__next._tree.txt +5 -0
- package/out/game/zhajinhua/__next.game.txt +5 -0
- package/out/game/zhajinhua/__next.game.zhajinhua.__PAGE__.txt +9 -0
- package/out/game/zhajinhua/__next.game.zhajinhua.txt +5 -0
- package/out/game/zhajinhua/index.html +15 -0
- package/out/game/zhajinhua/index.txt +25 -0
- package/out/index.html +2 -2
- package/out/index.txt +13 -13
- package/out/note/__next._full.txt +12 -12
- package/out/note/__next._head.txt +3 -3
- package/out/note/__next._index.txt +6 -6
- package/out/note/__next._tree.txt +1 -1
- package/out/note/__next.note.__PAGE__.txt +4 -4
- package/out/note/__next.note.txt +3 -3
- package/out/note/index.html +2 -2
- package/out/note/index.txt +12 -12
- package/out/ping/__next._full.txt +12 -12
- package/out/ping/__next._head.txt +3 -3
- package/out/ping/__next._index.txt +6 -6
- package/out/ping/__next._tree.txt +1 -1
- package/out/ping/__next.ping.__PAGE__.txt +4 -4
- package/out/ping/__next.ping.txt +3 -3
- package/out/ping/index.html +2 -2
- package/out/ping/index.txt +12 -12
- package/out/web3/__next._full.txt +12 -12
- package/out/web3/__next._head.txt +3 -3
- package/out/web3/__next._index.txt +6 -6
- package/out/web3/__next._tree.txt +1 -1
- package/out/web3/__next.web3.__PAGE__.txt +4 -4
- package/out/web3/__next.web3.txt +3 -3
- package/out/web3/ed25519/__next._full.txt +10 -10
- package/out/web3/ed25519/__next._head.txt +3 -3
- package/out/web3/ed25519/__next._index.txt +6 -6
- package/out/web3/ed25519/__next._tree.txt +1 -1
- package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
- package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
- package/out/web3/ed25519/__next.web3.txt +3 -3
- package/out/web3/ed25519/index.html +1 -1
- package/out/web3/ed25519/index.txt +10 -10
- package/out/web3/index.html +2 -2
- package/out/web3/index.txt +12 -12
- package/out/web3/tools/__next._full.txt +10 -10
- package/out/web3/tools/__next._head.txt +3 -3
- package/out/web3/tools/__next._index.txt +6 -6
- package/out/web3/tools/__next._tree.txt +1 -1
- package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
- package/out/web3/tools/__next.web3.tools.txt +3 -3
- package/out/web3/tools/__next.web3.txt +3 -3
- package/out/web3/tools/index.html +1 -1
- package/out/web3/tools/index.txt +10 -10
- package/package.json +2 -1
- package/server/index.js +0 -8
- package/server/src/config.js +1 -1
- package/server/src/core/gameRoom.js +222 -0
- package/server/src/core/zhajinhua.js +563 -0
- package/server/src/games/gandengyan.js +348 -411
- package/server/src/http/app.js +7 -1
- package/server/src/index.js +9 -1
- package/out/_next/static/chunks/04mo7rr..0_1q.js +0 -1
- package/out/_next/static/chunks/06rf3qq5ggs6v.js +0 -1
- package/out/_next/static/chunks/07td.jq7xff84.css +0 -1
- package/out/_next/static/chunks/0cl7d~7abnk_p.css +0 -1
- package/out/_next/static/chunks/0d306t1wvjpdx.js +0 -1
- package/out/_next/static/chunks/0n.ayxmsar6e5.js +0 -1
- package/out/_next/static/chunks/0voe1.ttrh84k.css +0 -1
- package/out/_next/static/chunks/0wtf0xsiicxx6.js +0 -1
- package/out/_next/static/chunks/0x.ky97owcxxs.js +0 -1
- package/out/_next/static/chunks/0ysj5b94vu4ri.js +0 -1
- package/out/_next/static/chunks/184hxsuf-5c84.js +0 -1
- package/out/gandengyan/__next._full.txt +0 -25
- package/out/gandengyan/__next._head.txt +0 -5
- package/out/gandengyan/__next._tree.txt +0 -5
- package/out/gandengyan/__next.gandengyan.__PAGE__.txt +0 -10
- package/out/gandengyan/__next.gandengyan.txt +0 -5
- package/out/gandengyan/index.html +0 -15
- package/out/gandengyan/index.txt +0 -25
- /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → sSvbBrwXZY-4lBmcHshga}/_buildManifest.js +0 -0
- /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → sSvbBrwXZY-4lBmcHshga}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → sSvbBrwXZY-4lBmcHshga}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import crypto from 'node:crypto'
|
|
2
|
-
|
|
3
1
|
const SUITS = ['S', 'H', 'C', 'D']
|
|
4
2
|
const RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2']
|
|
5
3
|
const STRAIGHT_RANKS = RANKS.filter(rank => rank !== '2')
|
|
@@ -7,167 +5,35 @@ const RANK_VALUE = new Map(RANKS.map((rank, index) => [rank, index + 3]))
|
|
|
7
5
|
const INITIAL_HAND_SIZE = 5
|
|
8
6
|
const SEALED_PENALTY = 15
|
|
9
7
|
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
export function createGanDengYanRoom({
|
|
9
|
+
roomCode,
|
|
10
|
+
ownerAddress,
|
|
11
|
+
ownerName,
|
|
12
|
+
players = [],
|
|
13
|
+
random = Math.random,
|
|
14
|
+
}) {
|
|
15
|
+
const roomPlayers = normalizePlayers(players)
|
|
16
|
+
if (!roomPlayers.some(player => player.address === normalizeAddress(ownerAddress))) {
|
|
17
|
+
roomPlayers.unshift({
|
|
18
|
+
address: normalizeAddress(ownerAddress),
|
|
19
|
+
name: cleanName(ownerName),
|
|
20
|
+
})
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
function unbindClient(ws) {
|
|
21
|
-
const client = clients.get(ws)
|
|
22
|
-
if (client?.roomId && rooms.has(client.roomId)) {
|
|
23
|
-
const room = rooms.get(client.roomId)
|
|
24
|
-
leaveRoom(room, client.id)
|
|
25
|
-
broadcastRoom(room)
|
|
26
|
-
}
|
|
27
|
-
clients.delete(ws)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function handleMessage(ws, event, data = {}) {
|
|
31
|
-
if (!event.startsWith('gandengyan:')) return false
|
|
32
|
-
const client = clients.get(ws)
|
|
33
|
-
if (!client) return true
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const identity = normalizeIdentity(data.identity)
|
|
37
|
-
if (identity.name) client.name = identity.name
|
|
38
|
-
if (identity.address) client.address = identity.address
|
|
39
|
-
|
|
40
|
-
switch (event) {
|
|
41
|
-
case 'gandengyan:createRoom': {
|
|
42
|
-
const room = createRoom({
|
|
43
|
-
roomId: makeRoomId(rooms),
|
|
44
|
-
ownerId: client.id,
|
|
45
|
-
ownerName: client.name,
|
|
46
|
-
ownerAddress: client.address,
|
|
47
|
-
})
|
|
48
|
-
rooms.set(room.id, room)
|
|
49
|
-
client.roomId = room.id
|
|
50
|
-
send(ws, 'gandengyan:roomCreated', { roomId: room.id })
|
|
51
|
-
broadcastRoom(room)
|
|
52
|
-
return true
|
|
53
|
-
}
|
|
54
|
-
case 'gandengyan:joinRoom': {
|
|
55
|
-
const roomId = String(data.roomId || '').trim().toUpperCase()
|
|
56
|
-
const room = rooms.get(roomId)
|
|
57
|
-
if (!room) throw new Error('房间不存在')
|
|
58
|
-
joinRoom(room, client.id, client.name, client.address)
|
|
59
|
-
client.roomId = room.id
|
|
60
|
-
broadcastRoom(room)
|
|
61
|
-
return true
|
|
62
|
-
}
|
|
63
|
-
default:
|
|
64
|
-
break
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const room = rooms.get(client.roomId)
|
|
68
|
-
if (!room) throw new Error('请先进入房间')
|
|
69
|
-
|
|
70
|
-
if (event === 'gandengyan:settings') {
|
|
71
|
-
if (room.ownerId !== client.id) throw new Error('只有房主可以修改设置')
|
|
72
|
-
setRoomSettings(room, data.settings || {})
|
|
73
|
-
broadcastRoom(room)
|
|
74
|
-
return true
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (event === 'gandengyan:start') {
|
|
78
|
-
if (room.ownerId !== client.id) throw new Error('只有房主可以开始')
|
|
79
|
-
startGame(room)
|
|
80
|
-
broadcastRoom(room)
|
|
81
|
-
scheduleBots(room, broadcastRoom)
|
|
82
|
-
return true
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (event === 'gandengyan:play') {
|
|
86
|
-
playCards(room, client.id, Array.isArray(data.cardIds) ? data.cardIds : [])
|
|
87
|
-
broadcastRoom(room)
|
|
88
|
-
scheduleBots(room, broadcastRoom)
|
|
89
|
-
return true
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (event === 'gandengyan:pass') {
|
|
93
|
-
passTurn(room, client.id)
|
|
94
|
-
broadcastRoom(room)
|
|
95
|
-
scheduleBots(room, broadcastRoom)
|
|
96
|
-
return true
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (event === 'gandengyan:restart') {
|
|
100
|
-
if (room.ownerId !== client.id) throw new Error('只有房主可以再来一局')
|
|
101
|
-
startGame(room)
|
|
102
|
-
broadcastRoom(room)
|
|
103
|
-
scheduleBots(room, broadcastRoom)
|
|
104
|
-
return true
|
|
105
|
-
}
|
|
106
|
-
} catch (error) {
|
|
107
|
-
send(ws, 'gandengyan:error', {
|
|
108
|
-
message: error instanceof Error ? error.message : '请求失败',
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return true
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function broadcastRoom(room) {
|
|
116
|
-
for (const [ws, client] of clients.entries()) {
|
|
117
|
-
if (client.roomId === room.id && ws.readyState === 1) {
|
|
118
|
-
send(ws, 'gandengyan:state', publicRoom(room, client.id))
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { bindClient, unbindClient, handleMessage }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function normalizeIdentity(identity) {
|
|
127
|
-
if (!identity || typeof identity !== 'object') return {}
|
|
128
|
-
return {
|
|
129
|
-
name: cleanName(identity.displayName || identity.username || identity.name),
|
|
130
|
-
address: String(identity.address || '').trim().slice(0, 64),
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function send(ws, event, data) {
|
|
135
|
-
if (ws.readyState === 1) {
|
|
136
|
-
ws.send(JSON.stringify({ event, data }))
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function scheduleBots(room, broadcastRoom) {
|
|
141
|
-
setTimeout(() => {
|
|
142
|
-
let moved = false
|
|
143
|
-
let guard = 0
|
|
144
|
-
while (room.status === 'playing' && guard < 8 && botStep(room)) {
|
|
145
|
-
moved = true
|
|
146
|
-
guard += 1
|
|
147
|
-
}
|
|
148
|
-
if (moved) broadcastRoom(room)
|
|
149
|
-
}, 650)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function createRoom({ roomId, ownerId, ownerName, ownerAddress }) {
|
|
153
23
|
const room = {
|
|
154
|
-
id:
|
|
155
|
-
|
|
24
|
+
id: String(roomCode || '').toUpperCase(),
|
|
25
|
+
ownerAddress: normalizeAddress(ownerAddress),
|
|
156
26
|
status: 'lobby',
|
|
157
|
-
|
|
158
|
-
players:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
score: 0,
|
|
168
|
-
playedCards: 0,
|
|
169
|
-
},
|
|
170
|
-
],
|
|
27
|
+
seq: 1,
|
|
28
|
+
players: roomPlayers.slice(0, 6).map((player, seat) => ({
|
|
29
|
+
address: player.address,
|
|
30
|
+
name: cleanName(player.name),
|
|
31
|
+
seat,
|
|
32
|
+
hand: [],
|
|
33
|
+
handCount: 0,
|
|
34
|
+
score: 0,
|
|
35
|
+
playedCards: 0,
|
|
36
|
+
})),
|
|
171
37
|
deck: [],
|
|
172
38
|
discard: [],
|
|
173
39
|
table: null,
|
|
@@ -181,185 +47,230 @@ function createRoom({ roomId, ownerId, ownerName, ownerAddress }) {
|
|
|
181
47
|
roundResult: null,
|
|
182
48
|
log: ['房间已创建'],
|
|
183
49
|
winnerSeat: null,
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
room.updatedAt = Date.now()
|
|
227
|
-
return player
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function leaveRoom(room, playerId) {
|
|
231
|
-
const player = room.players.find(item => item.id === playerId)
|
|
232
|
-
if (!player || player.bot) return
|
|
233
|
-
player.connected = false
|
|
234
|
-
if (room.status === 'lobby') {
|
|
235
|
-
room.players = room.players.filter(item => item.id !== playerId)
|
|
236
|
-
syncBots(room)
|
|
237
|
-
}
|
|
238
|
-
room.updatedAt = Date.now()
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function startGame(room) {
|
|
242
|
-
if (room.players.filter(player => !player.bot).length < 1) {
|
|
243
|
-
throw new Error('至少需要 1 名真人玩家')
|
|
244
|
-
}
|
|
245
|
-
syncBots(room)
|
|
246
|
-
room.deck = shuffle(createDeck(room.settings.decks))
|
|
247
|
-
room.discard = []
|
|
248
|
-
room.table = null
|
|
249
|
-
room.passSeats = []
|
|
250
|
-
room.winnerSeat = null
|
|
251
|
-
room.roundResult = null
|
|
252
|
-
room.baseScore = 1
|
|
253
|
-
room.bombCount = 0
|
|
254
|
-
room.status = 'playing'
|
|
255
|
-
for (const player of orderedPlayers(room)) {
|
|
256
|
-
player.hand = draw(room, INITIAL_HAND_SIZE)
|
|
50
|
+
random,
|
|
51
|
+
}
|
|
52
|
+
return publicGanDengYanRoom(room)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function syncGanDengYanLobby(room, players = []) {
|
|
56
|
+
const state = hydrateGanDengYanRoom(room)
|
|
57
|
+
if (!state || state.status !== 'lobby') return state
|
|
58
|
+
const currentScores = new Map(state.players.map(player => [player.address, player.score]))
|
|
59
|
+
state.players = normalizePlayers(players)
|
|
60
|
+
.slice(0, 6)
|
|
61
|
+
.map((player, seat) => ({
|
|
62
|
+
address: player.address,
|
|
63
|
+
name: cleanName(player.name),
|
|
64
|
+
seat,
|
|
65
|
+
hand: [],
|
|
66
|
+
handCount: 0,
|
|
67
|
+
score: currentScores.get(player.address) || 0,
|
|
68
|
+
playedCards: 0,
|
|
69
|
+
}))
|
|
70
|
+
state.seq += 1
|
|
71
|
+
return publicGanDengYanRoom(state)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function startGanDengYanRound(room, random = Math.random) {
|
|
75
|
+
const state = hydrateGanDengYanRoom(room)
|
|
76
|
+
if (!state || state.players.length < 2) {
|
|
77
|
+
throw new Error('至少需要 2 名玩家')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
state.deck = shuffle(createDeck(), random)
|
|
81
|
+
state.discard = []
|
|
82
|
+
state.table = null
|
|
83
|
+
state.passSeats = []
|
|
84
|
+
state.winnerSeat = null
|
|
85
|
+
state.roundResult = null
|
|
86
|
+
state.baseScore = 1
|
|
87
|
+
state.bombCount = 0
|
|
88
|
+
state.status = 'playing'
|
|
89
|
+
for (const player of orderedPlayers(state)) {
|
|
90
|
+
player.hand = draw(state, INITIAL_HAND_SIZE)
|
|
91
|
+
player.handCount = player.hand.length
|
|
257
92
|
player.playedCards = 0
|
|
258
93
|
sortHand(player.hand)
|
|
259
94
|
}
|
|
260
|
-
const starter = chooseStarter(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
95
|
+
const starter = chooseStarter(state, random)
|
|
96
|
+
state.currentSeat = starter.seat
|
|
97
|
+
state.lastWinnerSeat = null
|
|
98
|
+
state.log = [`新一局开始,${starter.name} 先出牌`]
|
|
99
|
+
if (state.diceRolls.length > 0) {
|
|
100
|
+
state.log.unshift(
|
|
101
|
+
`骰子结果:${state.diceRolls
|
|
102
|
+
.map(roll => `${roll.name} ${roll.value}`)
|
|
103
|
+
.join(',')}`
|
|
104
|
+
)
|
|
266
105
|
}
|
|
267
|
-
|
|
106
|
+
state.seq += 1
|
|
107
|
+
return publicGanDengYanRoom(state)
|
|
268
108
|
}
|
|
269
109
|
|
|
270
|
-
function
|
|
271
|
-
const
|
|
272
|
-
|
|
110
|
+
export function playGanDengYanCards(room, address, cardIds) {
|
|
111
|
+
const state = hydrateGanDengYanRoom(room)
|
|
112
|
+
const player = currentPlayer(state)
|
|
113
|
+
const normalizedAddress = normalizeAddress(address)
|
|
114
|
+
if (!player || player.address !== normalizedAddress) {
|
|
115
|
+
return { ok: false, error: '还没轮到你', state: publicGanDengYanRoom(state) }
|
|
116
|
+
}
|
|
117
|
+
|
|
273
118
|
const cards = cardIds.map(id => player.hand.find(card => card.id === id))
|
|
274
|
-
if (cards.length === 0 || cards.some(card => !card))
|
|
119
|
+
if (cards.length === 0 || cards.some(card => !card)) {
|
|
120
|
+
return { ok: false, error: '手牌不存在', state: publicGanDengYanRoom(state) }
|
|
121
|
+
}
|
|
275
122
|
const combo = analyzeCards(cards)
|
|
276
|
-
if (!combo)
|
|
277
|
-
|
|
123
|
+
if (!combo) {
|
|
124
|
+
return { ok: false, error: '这个牌型不合法', state: publicGanDengYanRoom(state) }
|
|
125
|
+
}
|
|
126
|
+
if (!canBeat(combo, state.table?.combo)) {
|
|
127
|
+
return { ok: false, error: '出的牌压不过上一手', state: publicGanDengYanRoom(state) }
|
|
128
|
+
}
|
|
129
|
+
|
|
278
130
|
player.hand = player.hand.filter(card => !cardIds.includes(card.id))
|
|
131
|
+
player.handCount = player.hand.length
|
|
279
132
|
player.playedCards += cards.length
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
133
|
+
state.discard.push(...cards)
|
|
134
|
+
state.table = { seat: player.seat, playerName: player.name, cards, combo }
|
|
135
|
+
state.passSeats = []
|
|
136
|
+
state.lastWinnerSeat = player.seat
|
|
284
137
|
if (combo.type === 'bomb') {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
138
|
+
state.bombCount += 1
|
|
139
|
+
state.baseScore *= 2
|
|
140
|
+
}
|
|
141
|
+
state.log.unshift(
|
|
142
|
+
`${player.name} 出 ${combo.label} ${cards.map(labelCard).join(' ')}${
|
|
143
|
+
combo.type === 'bomb' ? `,底分 ${state.baseScore}` : ''
|
|
144
|
+
}`
|
|
145
|
+
)
|
|
289
146
|
if (player.hand.length === 0) {
|
|
290
|
-
finishGame(
|
|
147
|
+
finishGame(state, player)
|
|
291
148
|
} else {
|
|
292
|
-
advanceTurn(
|
|
293
|
-
}
|
|
294
|
-
room.updatedAt = Date.now()
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function passTurn(room, playerId) {
|
|
298
|
-
const player = currentPlayer(room)
|
|
299
|
-
if (!player || player.id !== playerId) throw new Error('还没轮到你')
|
|
300
|
-
if (!room.table) throw new Error('领出时不能不要')
|
|
301
|
-
if (!room.passSeats.includes(player.seat)) room.passSeats.push(player.seat)
|
|
302
|
-
room.log.unshift(`${player.name} 不要`)
|
|
303
|
-
const activeSeats = activePlayers(room).map(item => item.seat)
|
|
304
|
-
const seatsToBeat = activeSeats.filter(seat => seat !== room.lastWinnerSeat)
|
|
305
|
-
if (seatsToBeat.every(seat => room.passSeats.includes(seat))) {
|
|
306
|
-
refillAfterRound(room)
|
|
307
|
-
room.currentSeat = room.lastWinnerSeat
|
|
308
|
-
room.table = null
|
|
309
|
-
room.passSeats = []
|
|
310
|
-
room.log.unshift('本轮结束,所有玩家各补 1 张,重新领出')
|
|
311
|
-
} else {
|
|
312
|
-
advanceTurn(room)
|
|
149
|
+
advanceTurn(state)
|
|
313
150
|
}
|
|
314
|
-
|
|
151
|
+
state.seq += 1
|
|
152
|
+
return { ok: true, state: publicGanDengYanRoom(state) }
|
|
315
153
|
}
|
|
316
154
|
|
|
317
|
-
function
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
if (
|
|
322
|
-
|
|
155
|
+
export function passGanDengYanTurn(room, address) {
|
|
156
|
+
const state = hydrateGanDengYanRoom(room)
|
|
157
|
+
const player = currentPlayer(state)
|
|
158
|
+
const normalizedAddress = normalizeAddress(address)
|
|
159
|
+
if (!player || player.address !== normalizedAddress) {
|
|
160
|
+
return { ok: false, error: '还没轮到你', state: publicGanDengYanRoom(state) }
|
|
161
|
+
}
|
|
162
|
+
if (!state.table) {
|
|
163
|
+
return { ok: false, error: '领出时不能不要', state: publicGanDengYanRoom(state) }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!state.passSeats.includes(player.seat)) state.passSeats.push(player.seat)
|
|
167
|
+
state.log.unshift(`${player.name} 不要`)
|
|
168
|
+
const activeSeats = activePlayers(state).map(item => item.seat)
|
|
169
|
+
const seatsToBeat = activeSeats.filter(seat => seat !== state.lastWinnerSeat)
|
|
170
|
+
if (seatsToBeat.every(seat => state.passSeats.includes(seat))) {
|
|
171
|
+
refillAfterRound(state)
|
|
172
|
+
state.currentSeat = state.lastWinnerSeat
|
|
173
|
+
state.table = null
|
|
174
|
+
state.passSeats = []
|
|
175
|
+
state.log.unshift('本轮结束,所有玩家各补 1 张,重新领出')
|
|
323
176
|
} else {
|
|
324
|
-
|
|
177
|
+
advanceTurn(state)
|
|
325
178
|
}
|
|
326
|
-
|
|
179
|
+
state.seq += 1
|
|
180
|
+
return { ok: true, state: publicGanDengYanRoom(state) }
|
|
327
181
|
}
|
|
328
182
|
|
|
329
|
-
function
|
|
183
|
+
export function publicGanDengYanRoom(room) {
|
|
184
|
+
if (!room) return null
|
|
330
185
|
return {
|
|
331
186
|
id: room.id,
|
|
332
|
-
|
|
187
|
+
ownerAddress: room.ownerAddress,
|
|
333
188
|
status: room.status,
|
|
334
|
-
|
|
335
|
-
deckCount: room.deck.
|
|
336
|
-
discardCount: room.discard.
|
|
337
|
-
currentSeat: room.currentSeat,
|
|
338
|
-
lastWinnerSeat:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
189
|
+
seq: Number(room.seq || 1),
|
|
190
|
+
deckCount: room.deck?.length || Number(room.deckCount || 0),
|
|
191
|
+
discardCount: room.discard?.length || Number(room.discardCount || 0),
|
|
192
|
+
currentSeat: Number(room.currentSeat || 0),
|
|
193
|
+
lastWinnerSeat:
|
|
194
|
+
room.lastWinnerSeat === null || room.lastWinnerSeat === undefined
|
|
195
|
+
? null
|
|
196
|
+
: Number(room.lastWinnerSeat),
|
|
197
|
+
previousWinnerSeat:
|
|
198
|
+
room.previousWinnerSeat === null || room.previousWinnerSeat === undefined
|
|
199
|
+
? null
|
|
200
|
+
: Number(room.previousWinnerSeat),
|
|
201
|
+
baseScore: Number(room.baseScore || 1),
|
|
202
|
+
bombCount: Number(room.bombCount || 0),
|
|
203
|
+
diceRolls: Array.isArray(room.diceRolls) ? room.diceRolls : [],
|
|
204
|
+
roundResult: room.roundResult || null,
|
|
205
|
+
table: room.table
|
|
206
|
+
? { ...room.table, cards: room.table.cards.map(publicCard) }
|
|
207
|
+
: null,
|
|
208
|
+
passSeats: Array.isArray(room.passSeats) ? room.passSeats : [],
|
|
209
|
+
winnerSeat:
|
|
210
|
+
room.winnerSeat === null || room.winnerSeat === undefined
|
|
211
|
+
? null
|
|
212
|
+
: Number(room.winnerSeat),
|
|
213
|
+
log: Array.isArray(room.log) ? room.log.slice(0, 18) : [],
|
|
348
214
|
players: orderedPlayers(room).map(player => ({
|
|
349
|
-
id: player.id,
|
|
350
215
|
address: player.address,
|
|
351
216
|
name: player.name,
|
|
352
|
-
bot: player.bot,
|
|
353
217
|
seat: player.seat,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
hand: player.id === viewerId ? player.hand.map(publicCard) : [],
|
|
218
|
+
handCount: player.hand?.length ?? player.handCount ?? 0,
|
|
219
|
+
score: Number(player.score || 0),
|
|
220
|
+
playedCards: Number(player.playedCards || 0),
|
|
221
|
+
hand: Array.isArray(player.hand) ? player.hand.map(publicCard) : [],
|
|
359
222
|
})),
|
|
360
223
|
}
|
|
361
224
|
}
|
|
362
225
|
|
|
226
|
+
export function hydrateGanDengYanRoom(input) {
|
|
227
|
+
if (!input || typeof input !== 'object') return null
|
|
228
|
+
return {
|
|
229
|
+
id: String(input.id || '').toUpperCase(),
|
|
230
|
+
ownerAddress: normalizeAddress(input.ownerAddress),
|
|
231
|
+
status:
|
|
232
|
+
input.status === 'playing' || input.status === 'finished'
|
|
233
|
+
? input.status
|
|
234
|
+
: 'lobby',
|
|
235
|
+
seq: Number(input.seq || 1),
|
|
236
|
+
players: Array.isArray(input.players)
|
|
237
|
+
? input.players.map(normalizeRoundPlayer).filter(Boolean)
|
|
238
|
+
: [],
|
|
239
|
+
deck: Array.isArray(input.deck) ? input.deck.map(normalizeCard).filter(Boolean) : [],
|
|
240
|
+
discard: Array.isArray(input.discard)
|
|
241
|
+
? input.discard.map(normalizeCard).filter(Boolean)
|
|
242
|
+
: [],
|
|
243
|
+
table:
|
|
244
|
+
input.table && typeof input.table === 'object'
|
|
245
|
+
? {
|
|
246
|
+
...input.table,
|
|
247
|
+
cards: Array.isArray(input.table.cards)
|
|
248
|
+
? input.table.cards.map(normalizeCard).filter(Boolean)
|
|
249
|
+
: [],
|
|
250
|
+
}
|
|
251
|
+
: null,
|
|
252
|
+
currentSeat: Number(input.currentSeat || 0),
|
|
253
|
+
lastWinnerSeat:
|
|
254
|
+
input.lastWinnerSeat === null || input.lastWinnerSeat === undefined
|
|
255
|
+
? null
|
|
256
|
+
: Number(input.lastWinnerSeat),
|
|
257
|
+
previousWinnerSeat:
|
|
258
|
+
input.previousWinnerSeat === null || input.previousWinnerSeat === undefined
|
|
259
|
+
? null
|
|
260
|
+
: Number(input.previousWinnerSeat),
|
|
261
|
+
passSeats: Array.isArray(input.passSeats) ? input.passSeats.map(Number) : [],
|
|
262
|
+
baseScore: Number(input.baseScore || 1),
|
|
263
|
+
bombCount: Number(input.bombCount || 0),
|
|
264
|
+
diceRolls: Array.isArray(input.diceRolls) ? input.diceRolls : [],
|
|
265
|
+
roundResult: input.roundResult || null,
|
|
266
|
+
log: Array.isArray(input.log) ? input.log.map(String) : [],
|
|
267
|
+
winnerSeat:
|
|
268
|
+
input.winnerSeat === null || input.winnerSeat === undefined
|
|
269
|
+
? null
|
|
270
|
+
: Number(input.winnerSeat),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
363
274
|
export function analyzeCards(cards) {
|
|
364
275
|
if (!cards?.length) return null
|
|
365
276
|
const jokerCount = cards.filter(isJoker).length
|
|
@@ -367,7 +278,9 @@ export function analyzeCards(cards) {
|
|
|
367
278
|
if (normals.length === 0) return null
|
|
368
279
|
const bomb = analyzeBomb(cards, normals, jokerCount)
|
|
369
280
|
if (bomb) return bomb
|
|
370
|
-
if (cards.length === 1 && jokerCount === 0)
|
|
281
|
+
if (cards.length === 1 && jokerCount === 0) {
|
|
282
|
+
return makeCombo('single', cardValue(cards[0]), 1, [cardValue(cards[0])])
|
|
283
|
+
}
|
|
371
284
|
if (cards.length === 2 && canRepresentSameRank(normals, jokerCount)) {
|
|
372
285
|
const value = sameRankValue(normals)
|
|
373
286
|
return makeCombo('pair', value, 2, [value, value])
|
|
@@ -378,14 +291,26 @@ export function analyzeCards(cards) {
|
|
|
378
291
|
function analyzeBomb(cards, normals, jokerCount) {
|
|
379
292
|
if (cards.length < 3 || !canRepresentSameRank(normals, jokerCount)) return null
|
|
380
293
|
const value = sameRankValue(normals)
|
|
381
|
-
return makeCombo(
|
|
294
|
+
return makeCombo(
|
|
295
|
+
'bomb',
|
|
296
|
+
value,
|
|
297
|
+
cards.length,
|
|
298
|
+
Array(cards.length).fill(value),
|
|
299
|
+
jokerCount === 0
|
|
300
|
+
)
|
|
382
301
|
}
|
|
383
302
|
|
|
384
303
|
function analyzeStraight(cards) {
|
|
385
304
|
if (cards.length < 3) return null
|
|
386
305
|
for (let start = 0; start <= STRAIGHT_RANKS.length - cards.length; start += 1) {
|
|
387
|
-
const values = STRAIGHT_RANKS.slice(start, start + cards.length).map(rank =>
|
|
388
|
-
|
|
306
|
+
const values = STRAIGHT_RANKS.slice(start, start + cards.length).map(rank =>
|
|
307
|
+
RANK_VALUE.get(rank)
|
|
308
|
+
)
|
|
309
|
+
if (
|
|
310
|
+
cards.every(
|
|
311
|
+
(card, index) => isJoker(card) || cardValue(card) === values[index]
|
|
312
|
+
)
|
|
313
|
+
) {
|
|
389
314
|
return makeCombo('straight', values.at(-1), cards.length, values)
|
|
390
315
|
}
|
|
391
316
|
}
|
|
@@ -396,9 +321,15 @@ function analyzePairStraight(cards) {
|
|
|
396
321
|
if (cards.length < 4 || cards.length % 2 !== 0) return null
|
|
397
322
|
const pairCount = cards.length / 2
|
|
398
323
|
for (let start = 0; start <= STRAIGHT_RANKS.length - pairCount; start += 1) {
|
|
399
|
-
const pairValues = STRAIGHT_RANKS.slice(start, start + pairCount).map(rank =>
|
|
324
|
+
const pairValues = STRAIGHT_RANKS.slice(start, start + pairCount).map(rank =>
|
|
325
|
+
RANK_VALUE.get(rank)
|
|
326
|
+
)
|
|
400
327
|
const values = pairValues.flatMap(value => [value, value])
|
|
401
|
-
if (
|
|
328
|
+
if (
|
|
329
|
+
cards.every(
|
|
330
|
+
(card, index) => isJoker(card) || cardValue(card) === values[index]
|
|
331
|
+
)
|
|
332
|
+
) {
|
|
402
333
|
return makeCombo('pairStraight', pairValues.at(-1), cards.length, values)
|
|
403
334
|
}
|
|
404
335
|
}
|
|
@@ -406,7 +337,14 @@ function analyzePairStraight(cards) {
|
|
|
406
337
|
}
|
|
407
338
|
|
|
408
339
|
function makeCombo(type, value, length, resolvedValues, pure = true) {
|
|
409
|
-
return {
|
|
340
|
+
return {
|
|
341
|
+
type,
|
|
342
|
+
value,
|
|
343
|
+
length,
|
|
344
|
+
resolvedValues,
|
|
345
|
+
pure,
|
|
346
|
+
label: labelCombo({ type, value, length, resolvedValues, pure }),
|
|
347
|
+
}
|
|
410
348
|
}
|
|
411
349
|
|
|
412
350
|
function canBeat(combo, tableCombo) {
|
|
@@ -426,34 +364,51 @@ function canBeat(combo, tableCombo) {
|
|
|
426
364
|
return combo.value === nextValue(tableCombo.value)
|
|
427
365
|
}
|
|
428
366
|
|
|
429
|
-
function
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
367
|
+
function normalizePlayers(players) {
|
|
368
|
+
const seen = new Set()
|
|
369
|
+
return players
|
|
370
|
+
.map(player => ({
|
|
371
|
+
address: normalizeAddress(player.address),
|
|
372
|
+
name: cleanName(player.name),
|
|
373
|
+
publicKey: String(player.publicKey || ''),
|
|
374
|
+
}))
|
|
375
|
+
.filter(player => {
|
|
376
|
+
if (!player.address || seen.has(player.address)) return false
|
|
377
|
+
seen.add(player.address)
|
|
378
|
+
return true
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function normalizeRoundPlayer(input) {
|
|
383
|
+
const address = normalizeAddress(input.address)
|
|
384
|
+
if (!address) return null
|
|
385
|
+
return {
|
|
386
|
+
address,
|
|
387
|
+
name: cleanName(input.name),
|
|
388
|
+
seat: Number(input.seat || 0),
|
|
389
|
+
hand: Array.isArray(input.hand) ? input.hand.map(normalizeCard).filter(Boolean) : [],
|
|
390
|
+
handCount: Number(input.handCount || 0),
|
|
391
|
+
score: Number(input.score || 0),
|
|
392
|
+
playedCards: Number(input.playedCards || 0),
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function normalizeCard(card) {
|
|
397
|
+
if (!card || typeof card !== 'object') return null
|
|
398
|
+
const rank = String(card.rank || '')
|
|
399
|
+
const suit = String(card.suit || '')
|
|
400
|
+
if (!RANK_VALUE.has(rank) && rank !== 'SJ' && rank !== 'BJ') return null
|
|
401
|
+
if (!SUITS.includes(suit) && suit !== 'Joker') return null
|
|
443
402
|
return {
|
|
444
|
-
id:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
connected: true,
|
|
450
|
-
hand: [],
|
|
451
|
-
score: 0,
|
|
452
|
-
playedCards: 0,
|
|
403
|
+
id: String(card.id || `${suit}-${rank}`),
|
|
404
|
+
suit,
|
|
405
|
+
rank,
|
|
406
|
+
label: card.label || labelCard({ suit, rank }),
|
|
407
|
+
color: card.color || cardColor({ suit, rank }),
|
|
453
408
|
}
|
|
454
409
|
}
|
|
455
410
|
|
|
456
|
-
function chooseStarter(room) {
|
|
411
|
+
function chooseStarter(room, random) {
|
|
457
412
|
if (room.previousWinnerSeat !== null) {
|
|
458
413
|
room.diceRolls = []
|
|
459
414
|
return orderedPlayers(room).find(player => player.seat === room.previousWinnerSeat) || orderedPlayers(room)[0]
|
|
@@ -461,14 +416,20 @@ function chooseStarter(room) {
|
|
|
461
416
|
let candidates = orderedPlayers(room)
|
|
462
417
|
let rolls = []
|
|
463
418
|
while (candidates.length > 1) {
|
|
464
|
-
rolls = candidates.map(player => ({
|
|
419
|
+
rolls = candidates.map(player => ({
|
|
420
|
+
seat: player.seat,
|
|
421
|
+
name: player.name,
|
|
422
|
+
value: rollDice(random),
|
|
423
|
+
}))
|
|
465
424
|
const max = Math.max(...rolls.map(roll => roll.value))
|
|
466
425
|
const winners = rolls.filter(roll => roll.value === max)
|
|
467
426
|
if (winners.length === 1) {
|
|
468
427
|
room.diceRolls = rolls
|
|
469
428
|
return candidates.find(player => player.seat === winners[0].seat)
|
|
470
429
|
}
|
|
471
|
-
candidates = candidates.filter(player =>
|
|
430
|
+
candidates = candidates.filter(player =>
|
|
431
|
+
winners.some(roll => roll.seat === player.seat)
|
|
432
|
+
)
|
|
472
433
|
}
|
|
473
434
|
room.diceRolls = rolls
|
|
474
435
|
return candidates[0]
|
|
@@ -486,10 +447,23 @@ function finishGame(room, winner) {
|
|
|
486
447
|
const loss = sealed ? SEALED_PENALTY : player.hand.length * room.baseScore
|
|
487
448
|
player.score -= loss
|
|
488
449
|
winnerGain += loss
|
|
489
|
-
losers.push({
|
|
450
|
+
losers.push({
|
|
451
|
+
seat: player.seat,
|
|
452
|
+
name: player.name,
|
|
453
|
+
loss,
|
|
454
|
+
sealed,
|
|
455
|
+
cardsLeft: player.hand.length,
|
|
456
|
+
})
|
|
490
457
|
}
|
|
491
458
|
winner.score += winnerGain
|
|
492
|
-
room.roundResult = {
|
|
459
|
+
room.roundResult = {
|
|
460
|
+
winnerSeat: winner.seat,
|
|
461
|
+
winnerName: winner.name,
|
|
462
|
+
winnerGain,
|
|
463
|
+
baseScore: room.baseScore,
|
|
464
|
+
bombCount: room.bombCount,
|
|
465
|
+
losers,
|
|
466
|
+
}
|
|
493
467
|
room.log.unshift(`${winner.name} 获胜,赢 ${winnerGain} 分`)
|
|
494
468
|
}
|
|
495
469
|
|
|
@@ -497,34 +471,27 @@ function refillAfterRound(room) {
|
|
|
497
471
|
for (const player of activePlayers(room)) {
|
|
498
472
|
if (room.deck.length === 0) break
|
|
499
473
|
player.hand.push(...draw(room, 1))
|
|
474
|
+
player.handCount = player.hand.length
|
|
500
475
|
sortHand(player.hand)
|
|
501
476
|
}
|
|
502
477
|
}
|
|
503
478
|
|
|
504
|
-
function
|
|
505
|
-
for (let seat = 0; seat < room.settings.seats; seat += 1) {
|
|
506
|
-
const player = room.players.find(item => item.seat === seat)
|
|
507
|
-
if (!player || player.bot) return seat
|
|
508
|
-
}
|
|
509
|
-
return -1
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function createDeck(decks) {
|
|
479
|
+
function createDeck() {
|
|
513
480
|
const cards = []
|
|
514
|
-
for (
|
|
515
|
-
for (const
|
|
516
|
-
|
|
481
|
+
for (const suit of SUITS) {
|
|
482
|
+
for (const rank of RANKS) {
|
|
483
|
+
cards.push({ id: `${suit}-${rank}`, suit, rank })
|
|
517
484
|
}
|
|
518
|
-
cards.push({ id: `${deck}-SJ`, suit: 'Joker', rank: 'SJ', deck })
|
|
519
|
-
cards.push({ id: `${deck}-BJ`, suit: 'Joker', rank: 'BJ', deck })
|
|
520
485
|
}
|
|
521
|
-
|
|
486
|
+
cards.push({ id: 'SJ', suit: 'Joker', rank: 'SJ' })
|
|
487
|
+
cards.push({ id: 'BJ', suit: 'Joker', rank: 'BJ' })
|
|
488
|
+
return cards.map(publicCard)
|
|
522
489
|
}
|
|
523
490
|
|
|
524
|
-
function shuffle(cards) {
|
|
491
|
+
function shuffle(cards, random) {
|
|
525
492
|
const copy = [...cards]
|
|
526
493
|
for (let index = copy.length - 1; index > 0; index -= 1) {
|
|
527
|
-
const swapIndex = Math.floor(
|
|
494
|
+
const swapIndex = Math.floor(random() * (index + 1))
|
|
528
495
|
;[copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]]
|
|
529
496
|
}
|
|
530
497
|
return copy
|
|
@@ -547,7 +514,7 @@ function activePlayers(room) {
|
|
|
547
514
|
}
|
|
548
515
|
|
|
549
516
|
function orderedPlayers(room) {
|
|
550
|
-
return [...room
|
|
517
|
+
return [...(room?.players || [])].sort((a, b) => a.seat - b.seat)
|
|
551
518
|
}
|
|
552
519
|
|
|
553
520
|
function advanceTurn(room) {
|
|
@@ -556,40 +523,7 @@ function advanceTurn(room) {
|
|
|
556
523
|
room.currentSeat = seats[(currentIndex + 1) % seats.length]
|
|
557
524
|
}
|
|
558
525
|
|
|
559
|
-
function
|
|
560
|
-
const candidates = enumerateBotMoves(hand)
|
|
561
|
-
return candidates.find(cards => canBeat(analyzeCards(cards), tableCombo)) || []
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function enumerateBotMoves(hand) {
|
|
565
|
-
const sorted = [...hand].sort(compareCards)
|
|
566
|
-
const moves = sorted.filter(card => !isJoker(card)).map(card => [card])
|
|
567
|
-
for (let size = 2; size <= Math.min(6, sorted.length); size += 1) {
|
|
568
|
-
for (const cards of combinations(sorted, size)) {
|
|
569
|
-
if (analyzeCards(cards)) moves.push(cards)
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
return moves.sort((a, b) => {
|
|
573
|
-
const comboA = analyzeCards(a)
|
|
574
|
-
const comboB = analyzeCards(b)
|
|
575
|
-
if (comboA.type === 'bomb' && comboB.type !== 'bomb') return 1
|
|
576
|
-
if (comboA.type !== 'bomb' && comboB.type === 'bomb') return -1
|
|
577
|
-
return comboA.length - comboB.length || comboA.value - comboB.value
|
|
578
|
-
})
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function combinations(cards, size, start = 0, prefix = [], output = []) {
|
|
582
|
-
if (prefix.length === size) {
|
|
583
|
-
output.push(prefix)
|
|
584
|
-
return output
|
|
585
|
-
}
|
|
586
|
-
for (let index = start; index <= cards.length - (size - prefix.length); index += 1) {
|
|
587
|
-
combinations(cards, size, index + 1, [...prefix, cards[index]], output)
|
|
588
|
-
}
|
|
589
|
-
return output
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function canRepresentSameRank(normals, jokerCount) {
|
|
526
|
+
function canRepresentSameRank(normals, jokerCount = 0) {
|
|
593
527
|
if (normals.length === 0) return false
|
|
594
528
|
const first = normals[0].rank
|
|
595
529
|
return normals.every(card => card.rank === first) && normals.length + jokerCount >= 2
|
|
@@ -616,7 +550,13 @@ function suitValue(suit) {
|
|
|
616
550
|
}
|
|
617
551
|
|
|
618
552
|
function publicCard(card) {
|
|
619
|
-
return {
|
|
553
|
+
return {
|
|
554
|
+
id: card.id,
|
|
555
|
+
suit: card.suit,
|
|
556
|
+
rank: card.rank,
|
|
557
|
+
label: labelCard(card),
|
|
558
|
+
color: cardColor(card),
|
|
559
|
+
}
|
|
620
560
|
}
|
|
621
561
|
|
|
622
562
|
function labelCard(card) {
|
|
@@ -630,11 +570,15 @@ function suitSymbol(suit) {
|
|
|
630
570
|
}
|
|
631
571
|
|
|
632
572
|
function cardColor(card) {
|
|
633
|
-
return card.suit === 'H' || card.suit === 'D' || card.rank === 'BJ'
|
|
573
|
+
return card.suit === 'H' || card.suit === 'D' || card.rank === 'BJ'
|
|
574
|
+
? 'red'
|
|
575
|
+
: 'black'
|
|
634
576
|
}
|
|
635
577
|
|
|
636
578
|
function labelCombo(combo) {
|
|
637
|
-
const resolved = combo.resolvedValues?.length
|
|
579
|
+
const resolved = combo.resolvedValues?.length
|
|
580
|
+
? `(${combo.resolvedValues.map(valueLabel).join(' ')})`
|
|
581
|
+
: ''
|
|
638
582
|
return `${{
|
|
639
583
|
single: '单张',
|
|
640
584
|
pair: '对子',
|
|
@@ -654,22 +598,15 @@ function nextValue(value) {
|
|
|
654
598
|
return RANK_VALUE.get(RANKS[index + 1])
|
|
655
599
|
}
|
|
656
600
|
|
|
657
|
-
function
|
|
658
|
-
|
|
659
|
-
do {
|
|
660
|
-
id = Math.random().toString(36).slice(2, 8).toUpperCase()
|
|
661
|
-
} while (rooms.has(id))
|
|
662
|
-
return id
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function cleanName(name) {
|
|
666
|
-
return String(name || '玩家').trim().slice(0, 16) || '玩家'
|
|
601
|
+
function rollDice(random) {
|
|
602
|
+
return Math.floor(random() * 6) + 1
|
|
667
603
|
}
|
|
668
604
|
|
|
669
|
-
function
|
|
670
|
-
|
|
605
|
+
function normalizeAddress(value) {
|
|
606
|
+
const address = String(value || '').trim()
|
|
607
|
+
return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''
|
|
671
608
|
}
|
|
672
609
|
|
|
673
|
-
function
|
|
674
|
-
return
|
|
610
|
+
function cleanName(name) {
|
|
611
|
+
return String(name || '玩家').trim().slice(0, 16) || '玩家'
|
|
675
612
|
}
|