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.
Files changed (171) hide show
  1. package/out/404/index.html +2 -2
  2. package/out/404.html +2 -2
  3. package/out/__next.__PAGE__.txt +5 -5
  4. package/out/__next._full.txt +13 -13
  5. package/out/__next._head.txt +3 -3
  6. package/out/__next._index.txt +6 -6
  7. package/out/__next._tree.txt +2 -2
  8. package/out/_next/static/chunks/02zzxfop_k6tl.css +1 -0
  9. package/out/_next/static/chunks/03gsbg0fr00ey.js +1 -0
  10. package/out/_next/static/chunks/06lttvu7563zo.css +1 -0
  11. package/out/_next/static/chunks/0fo9-h4knidcz.js +1 -0
  12. package/out/_next/static/chunks/0hyds~bp.auvh.js +1 -0
  13. package/out/_next/static/chunks/0i9sfdypwuw8~.js +1 -0
  14. package/out/_next/static/chunks/{0_0oph_z1az14.js → 0puk.7e.tr2zy.js} +1 -1
  15. package/out/_next/static/chunks/0s1k6rlwy02c2.js +1 -0
  16. package/out/_next/static/chunks/0sgltmtk_9s8p.css +1 -0
  17. package/out/_next/static/chunks/0snehvtvu1n4q.js +1 -0
  18. package/out/_next/static/chunks/{12nr19.nnn6s3.js → 0t_3xxx4zkerp.js} +2 -2
  19. package/out/_next/static/chunks/0u38kke9vhobe.js +1 -0
  20. package/out/_next/static/chunks/0vd4_a5x-wpdh.js +1 -0
  21. package/out/_next/static/chunks/0xx_10jns1.s7.css +1 -0
  22. package/out/_next/static/chunks/{0m_5nb6x8qy._.js → 10f-t2n4y1zx8.js} +1 -1
  23. package/out/_next/static/chunks/13jdyag9a-~kk.js +1 -0
  24. package/out/_not-found/__next._full.txt +11 -11
  25. package/out/_not-found/__next._head.txt +3 -3
  26. package/out/_not-found/__next._index.txt +6 -6
  27. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  28. package/out/_not-found/__next._not-found.txt +3 -3
  29. package/out/_not-found/__next._tree.txt +1 -1
  30. package/out/_not-found/index.html +2 -2
  31. package/out/_not-found/index.txt +11 -11
  32. package/out/admin/__next._full.txt +12 -12
  33. package/out/admin/__next._head.txt +3 -3
  34. package/out/admin/__next._index.txt +6 -6
  35. package/out/admin/__next._tree.txt +1 -1
  36. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  37. package/out/admin/__next.admin.txt +3 -3
  38. package/out/admin/index.html +2 -2
  39. package/out/admin/index.txt +12 -12
  40. package/out/app/__next._full.txt +12 -12
  41. package/out/app/__next._head.txt +3 -3
  42. package/out/app/__next._index.txt +6 -6
  43. package/out/app/__next._tree.txt +1 -1
  44. package/out/app/__next.app.__PAGE__.txt +4 -4
  45. package/out/app/__next.app.txt +3 -3
  46. package/out/app/index.html +2 -2
  47. package/out/app/index.txt +12 -12
  48. package/out/chat/__next._full.txt +12 -12
  49. package/out/chat/__next._head.txt +3 -3
  50. package/out/chat/__next._index.txt +6 -6
  51. package/out/chat/__next._tree.txt +1 -1
  52. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  53. package/out/chat/__next.chat.txt +3 -3
  54. package/out/chat/index.html +2 -2
  55. package/out/chat/index.txt +12 -12
  56. package/out/chat/join/__next._full.txt +12 -12
  57. package/out/chat/join/__next._head.txt +3 -3
  58. package/out/chat/join/__next._index.txt +6 -6
  59. package/out/chat/join/__next._tree.txt +1 -1
  60. package/out/chat/join/__next.chat.join.__PAGE__.txt +4 -4
  61. package/out/chat/join/__next.chat.join.txt +3 -3
  62. package/out/chat/join/__next.chat.txt +3 -3
  63. package/out/chat/join/index.html +2 -2
  64. package/out/chat/join/index.txt +12 -12
  65. package/out/download/__next._full.txt +16 -16
  66. package/out/download/__next._head.txt +3 -3
  67. package/out/download/__next._index.txt +6 -6
  68. package/out/download/__next._tree.txt +2 -2
  69. package/out/download/__next.download.__PAGE__.txt +6 -6
  70. package/out/download/__next.download.txt +3 -3
  71. package/out/download/index.html +2 -2
  72. package/out/download/index.txt +16 -16
  73. package/out/game/__next._full.txt +20 -0
  74. package/out/game/__next._head.txt +5 -0
  75. package/out/{gandengyan → game}/__next._index.txt +6 -6
  76. package/out/game/__next._tree.txt +5 -0
  77. package/out/game/__next.game.__PAGE__.txt +6 -0
  78. package/out/game/__next.game.txt +5 -0
  79. package/out/game/gandengyan/__next._full.txt +26 -0
  80. package/out/game/gandengyan/__next._head.txt +5 -0
  81. package/out/game/gandengyan/__next._index.txt +9 -0
  82. package/out/game/gandengyan/__next._tree.txt +6 -0
  83. package/out/game/gandengyan/__next.game.gandengyan.__PAGE__.txt +10 -0
  84. package/out/game/gandengyan/__next.game.gandengyan.txt +5 -0
  85. package/out/game/gandengyan/__next.game.txt +5 -0
  86. package/out/game/gandengyan/index.html +15 -0
  87. package/out/game/gandengyan/index.txt +26 -0
  88. package/out/game/index.html +1 -0
  89. package/out/game/index.txt +20 -0
  90. package/out/game/zhajinhua/__next._full.txt +25 -0
  91. package/out/game/zhajinhua/__next._head.txt +5 -0
  92. package/out/game/zhajinhua/__next._index.txt +9 -0
  93. package/out/game/zhajinhua/__next._tree.txt +5 -0
  94. package/out/game/zhajinhua/__next.game.txt +5 -0
  95. package/out/game/zhajinhua/__next.game.zhajinhua.__PAGE__.txt +9 -0
  96. package/out/game/zhajinhua/__next.game.zhajinhua.txt +5 -0
  97. package/out/game/zhajinhua/index.html +15 -0
  98. package/out/game/zhajinhua/index.txt +25 -0
  99. package/out/index.html +2 -2
  100. package/out/index.txt +13 -13
  101. package/out/note/__next._full.txt +12 -12
  102. package/out/note/__next._head.txt +3 -3
  103. package/out/note/__next._index.txt +6 -6
  104. package/out/note/__next._tree.txt +1 -1
  105. package/out/note/__next.note.__PAGE__.txt +4 -4
  106. package/out/note/__next.note.txt +3 -3
  107. package/out/note/index.html +2 -2
  108. package/out/note/index.txt +12 -12
  109. package/out/ping/__next._full.txt +12 -12
  110. package/out/ping/__next._head.txt +3 -3
  111. package/out/ping/__next._index.txt +6 -6
  112. package/out/ping/__next._tree.txt +1 -1
  113. package/out/ping/__next.ping.__PAGE__.txt +4 -4
  114. package/out/ping/__next.ping.txt +3 -3
  115. package/out/ping/index.html +2 -2
  116. package/out/ping/index.txt +12 -12
  117. package/out/web3/__next._full.txt +12 -12
  118. package/out/web3/__next._head.txt +3 -3
  119. package/out/web3/__next._index.txt +6 -6
  120. package/out/web3/__next._tree.txt +1 -1
  121. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  122. package/out/web3/__next.web3.txt +3 -3
  123. package/out/web3/ed25519/__next._full.txt +10 -10
  124. package/out/web3/ed25519/__next._head.txt +3 -3
  125. package/out/web3/ed25519/__next._index.txt +6 -6
  126. package/out/web3/ed25519/__next._tree.txt +1 -1
  127. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  128. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  129. package/out/web3/ed25519/__next.web3.txt +3 -3
  130. package/out/web3/ed25519/index.html +1 -1
  131. package/out/web3/ed25519/index.txt +10 -10
  132. package/out/web3/index.html +2 -2
  133. package/out/web3/index.txt +12 -12
  134. package/out/web3/tools/__next._full.txt +10 -10
  135. package/out/web3/tools/__next._head.txt +3 -3
  136. package/out/web3/tools/__next._index.txt +6 -6
  137. package/out/web3/tools/__next._tree.txt +1 -1
  138. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  139. package/out/web3/tools/__next.web3.tools.txt +3 -3
  140. package/out/web3/tools/__next.web3.txt +3 -3
  141. package/out/web3/tools/index.html +1 -1
  142. package/out/web3/tools/index.txt +10 -10
  143. package/package.json +2 -1
  144. package/server/index.js +0 -8
  145. package/server/src/config.js +1 -1
  146. package/server/src/core/gameRoom.js +222 -0
  147. package/server/src/core/zhajinhua.js +563 -0
  148. package/server/src/games/gandengyan.js +348 -411
  149. package/server/src/http/app.js +7 -1
  150. package/server/src/index.js +9 -1
  151. package/out/_next/static/chunks/04mo7rr..0_1q.js +0 -1
  152. package/out/_next/static/chunks/06rf3qq5ggs6v.js +0 -1
  153. package/out/_next/static/chunks/07td.jq7xff84.css +0 -1
  154. package/out/_next/static/chunks/0cl7d~7abnk_p.css +0 -1
  155. package/out/_next/static/chunks/0d306t1wvjpdx.js +0 -1
  156. package/out/_next/static/chunks/0n.ayxmsar6e5.js +0 -1
  157. package/out/_next/static/chunks/0voe1.ttrh84k.css +0 -1
  158. package/out/_next/static/chunks/0wtf0xsiicxx6.js +0 -1
  159. package/out/_next/static/chunks/0x.ky97owcxxs.js +0 -1
  160. package/out/_next/static/chunks/0ysj5b94vu4ri.js +0 -1
  161. package/out/_next/static/chunks/184hxsuf-5c84.js +0 -1
  162. package/out/gandengyan/__next._full.txt +0 -25
  163. package/out/gandengyan/__next._head.txt +0 -5
  164. package/out/gandengyan/__next._tree.txt +0 -5
  165. package/out/gandengyan/__next.gandengyan.__PAGE__.txt +0 -10
  166. package/out/gandengyan/__next.gandengyan.txt +0 -5
  167. package/out/gandengyan/index.html +0 -15
  168. package/out/gandengyan/index.txt +0 -25
  169. /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → sSvbBrwXZY-4lBmcHshga}/_buildManifest.js +0 -0
  170. /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → sSvbBrwXZY-4lBmcHshga}/_clientMiddlewareManifest.js +0 -0
  171. /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 createGanDengYanSocketHandlers() {
11
- const rooms = new Map()
12
- const clients = new Map()
13
-
14
- function bindClient(ws) {
15
- const id = crypto.randomUUID()
16
- clients.set(ws, { id, roomId: null, name: '玩家', address: '' })
17
- send(ws, 'gandengyan:hello', { playerId: id })
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: roomId,
155
- ownerId,
24
+ id: String(roomCode || '').toUpperCase(),
25
+ ownerAddress: normalizeAddress(ownerAddress),
156
26
  status: 'lobby',
157
- settings: { decks: 1, seats: 2, bots: 1 },
158
- players: [
159
- {
160
- id: ownerId,
161
- address: ownerAddress || '',
162
- name: ownerName || '玩家',
163
- bot: false,
164
- seat: 0,
165
- connected: true,
166
- hand: [],
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
- updatedAt: Date.now(),
185
- }
186
- syncBots(room)
187
- return room
188
- }
189
-
190
- function setRoomSettings(room, settings) {
191
- if (room.status !== 'lobby') throw new Error('游戏开始后不能修改设置')
192
- room.settings.decks = clamp(Number(settings.decks) || 1, 1, 2)
193
- room.settings.seats = clamp(Number(settings.seats) || 2, 2, 6)
194
- room.settings.bots = clamp(Number(settings.bots) || 0, 0, room.settings.seats - 1)
195
- syncBots(room)
196
- room.updatedAt = Date.now()
197
- }
198
-
199
- function joinRoom(room, playerId, name, address) {
200
- let player = room.players.find(item => item.id === playerId)
201
- if (player) {
202
- player.connected = true
203
- if (name) player.name = name
204
- if (address) player.address = address
205
- return player
206
- }
207
- if (room.status !== 'lobby') throw new Error('游戏已经开始')
208
- const seat = firstOpenHumanSeat(room)
209
- if (seat === -1) throw new Error('房间已满')
210
- player = {
211
- id: playerId,
212
- address: address || '',
213
- name: name || `玩家${seat + 1}`,
214
- bot: false,
215
- seat,
216
- connected: true,
217
- hand: [],
218
- score: 0,
219
- playedCards: 0,
220
- }
221
- room.players = room.players.filter(item => item.seat !== seat)
222
- room.players.push(player)
223
- room.players.sort((a, b) => a.seat - b.seat)
224
- syncBots(room)
225
- room.log.unshift(`${player.name} 加入牌桌`)
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(room)
261
- room.currentSeat = starter.seat
262
- room.lastWinnerSeat = null
263
- room.log = [`新一局开始,${starter.name} 先出牌`]
264
- if (room.diceRolls.length > 0) {
265
- room.log.unshift(`骰子结果:${room.diceRolls.map(roll => `${roll.name} ${roll.value}`).join(',')}`)
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
- room.updatedAt = Date.now()
106
+ state.seq += 1
107
+ return publicGanDengYanRoom(state)
268
108
  }
269
109
 
270
- function playCards(room, playerId, cardIds) {
271
- const player = currentPlayer(room)
272
- if (!player || player.id !== playerId) throw new Error('还没轮到你')
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)) throw new Error('手牌不存在')
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) throw new Error('这个牌型不合法')
277
- if (!canBeat(combo, room.table?.combo)) throw new Error('出的牌压不过上一手')
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
- room.discard.push(...cards)
281
- room.table = { seat: player.seat, playerName: player.name, cards, combo }
282
- room.passSeats = []
283
- room.lastWinnerSeat = player.seat
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
- room.bombCount += 1
286
- room.baseScore *= 2
287
- }
288
- room.log.unshift(`${player.name} 出 ${combo.label} ${cards.map(labelCard).join(' ')}${combo.type === 'bomb' ? `,底分 ${room.baseScore}` : ''}`)
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(room, player)
147
+ finishGame(state, player)
291
148
  } else {
292
- advanceTurn(room)
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
- room.updatedAt = Date.now()
151
+ state.seq += 1
152
+ return { ok: true, state: publicGanDengYanRoom(state) }
315
153
  }
316
154
 
317
- function botStep(room) {
318
- const player = currentPlayer(room)
319
- if (!player?.bot || room.status !== 'playing') return false
320
- const move = chooseBotMove(player.hand, room.table?.combo)
321
- if (move.length > 0) {
322
- playCards(room, player.id, move.map(card => card.id))
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
- passTurn(room, player.id)
177
+ advanceTurn(state)
325
178
  }
326
- return true
179
+ state.seq += 1
180
+ return { ok: true, state: publicGanDengYanRoom(state) }
327
181
  }
328
182
 
329
- function publicRoom(room, viewerId) {
183
+ export function publicGanDengYanRoom(room) {
184
+ if (!room) return null
330
185
  return {
331
186
  id: room.id,
332
- ownerId: room.ownerId,
187
+ ownerAddress: room.ownerAddress,
333
188
  status: room.status,
334
- settings: room.settings,
335
- deckCount: room.deck.length,
336
- discardCount: room.discard.length,
337
- currentSeat: room.currentSeat,
338
- lastWinnerSeat: room.lastWinnerSeat,
339
- previousWinnerSeat: room.previousWinnerSeat,
340
- baseScore: room.baseScore,
341
- bombCount: room.bombCount,
342
- diceRolls: room.diceRolls,
343
- roundResult: room.roundResult,
344
- table: room.table ? { ...room.table, cards: room.table.cards.map(publicCard) } : null,
345
- passSeats: room.passSeats,
346
- winnerSeat: room.winnerSeat,
347
- log: room.log.slice(0, 18),
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
- connected: player.connected,
355
- handCount: player.hand.length,
356
- score: player.score,
357
- playedCards: player.playedCards,
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) return makeCombo('single', cardValue(cards[0]), 1, [cardValue(cards[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('bomb', value, cards.length, Array(cards.length).fill(value), jokerCount === 0)
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 => RANK_VALUE.get(rank))
388
- if (cards.every((card, index) => isJoker(card) || cardValue(card) === values[index])) {
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 => RANK_VALUE.get(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 (cards.every((card, index) => isJoker(card) || cardValue(card) === values[index])) {
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 { type, value, length, resolvedValues, pure, label: labelCombo({ type, value, length, resolvedValues, pure }) }
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 syncBots(room) {
430
- const seats = room.settings.seats
431
- room.players = room.players.filter(player => player.seat < seats && (!player.bot || room.status === 'lobby'))
432
- while (orderedPlayers(room).filter(player => player.bot).length > room.settings.bots) {
433
- const bot = orderedPlayers(room).filter(player => player.bot).at(-1)
434
- room.players = room.players.filter(player => player.id !== bot.id)
435
- }
436
- for (let seat = 0; orderedPlayers(room).filter(player => player.bot).length < room.settings.bots && seat < seats; seat += 1) {
437
- if (!room.players.find(player => player.seat === seat)) room.players.push(makeBot(seat))
438
- }
439
- room.players.sort((a, b) => a.seat - b.seat)
440
- }
441
-
442
- function makeBot(seat) {
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: `bot-${seat}-${Math.random().toString(36).slice(2, 8)}`,
445
- address: '',
446
- name: `人机${seat + 1}`,
447
- bot: true,
448
- seat,
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 => ({ seat: player.seat, name: player.name, value: rollDice() }))
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 => winners.some(roll => roll.seat === player.seat))
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({ seat: player.seat, name: player.name, loss, sealed, cardsLeft: player.hand.length })
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 = { winnerSeat: winner.seat, winnerName: winner.name, winnerGain, baseScore: room.baseScore, bombCount: room.bombCount, losers }
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 firstOpenHumanSeat(room) {
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 (let deck = 0; deck < decks; deck += 1) {
515
- for (const suit of SUITS) {
516
- for (const rank of RANKS) cards.push({ id: `${deck}-${suit}-${rank}`, suit, rank, deck })
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
- return cards
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(Math.random() * (index + 1))
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.players].sort((a, b) => a.seat - b.seat)
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 chooseBotMove(hand, tableCombo) {
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 { id: card.id, suit: card.suit, rank: card.rank, label: labelCard(card), color: cardColor(card) }
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' ? 'red' : 'black'
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 ? `(${combo.resolvedValues.map(valueLabel).join(' ')})` : ''
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 makeRoomId(rooms) {
658
- let id = ''
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 rollDice() {
670
- return Math.floor(Math.random() * 6) + 1
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 clamp(value, min, max) {
674
- return Math.max(min, Math.min(max, value))
610
+ function cleanName(name) {
611
+ return String(name || '玩家').trim().slice(0, 16) || '玩家'
675
612
  }