most-box 0.1.3 → 0.1.5

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 (205) hide show
  1. package/README.md +7 -4
  2. package/electron/main.js +31 -10
  3. package/out/404/index.html +2 -2
  4. package/out/404.html +2 -2
  5. package/out/__next.__PAGE__.txt +6 -6
  6. package/out/__next._full.txt +15 -15
  7. package/out/__next._head.txt +3 -3
  8. package/out/__next._index.txt +7 -7
  9. package/out/__next._tree.txt +4 -4
  10. package/out/_next/static/chunks/{0hpev4am9jpmu.css → 0.4ov9319fecg.css} +1 -1
  11. package/out/_next/static/chunks/0._8s6pbfw.xk.js +1 -0
  12. package/out/_next/static/chunks/02jvyg27pp0mz.js +1 -0
  13. package/out/_next/static/chunks/02zzxfop_k6tl.css +1 -0
  14. package/out/_next/static/chunks/06lttvu7563zo.css +1 -0
  15. package/out/_next/static/chunks/{0pt.5cg1t09qs.js → 08qqf0rsi3oot.js} +1 -1
  16. package/out/_next/static/chunks/{0adx~d-j05c9d.css → 09h7l4i38xc7q.css} +1 -1
  17. package/out/_next/static/chunks/09hqx79-jkvm_.css +1 -0
  18. package/out/_next/static/chunks/09vcm1ku9k7o8.js +1 -0
  19. package/out/_next/static/chunks/0c6e_d2jq179x.js +1 -0
  20. package/out/_next/static/chunks/0ccalho416.d7.js +1 -0
  21. package/out/_next/static/chunks/0exj_tg.ew-t3.js +1 -0
  22. package/out/_next/static/chunks/0f-wz5d~tv-r4.js +1 -0
  23. package/out/_next/static/chunks/0f0jhsujtf-61.js +1 -0
  24. package/out/_next/static/chunks/0hyds~bp.auvh.js +1 -0
  25. package/out/_next/static/chunks/0ig8a4sazk3~2.css +1 -0
  26. package/out/_next/static/chunks/0iq1h7g4dudg8.js +1 -0
  27. package/out/_next/static/chunks/0knnzo9aih48r.js +1 -0
  28. package/out/_next/static/chunks/0nzyk~sg_tn._.js +1 -0
  29. package/out/_next/static/chunks/{12nr19.nnn6s3.js → 0t_3xxx4zkerp.js} +2 -2
  30. package/out/_next/static/chunks/0w~2fjq86t7c7.css +1 -0
  31. package/out/_next/static/chunks/0xrqerhosrp9~.js +1 -0
  32. package/out/_next/static/chunks/0xx_10jns1.s7.css +1 -0
  33. package/out/_next/static/chunks/14-fm9r_mom81.js +1 -0
  34. package/out/_next/static/chunks/14jhy~xia8lh8.js +1 -0
  35. package/out/_next/static/chunks/{turbopack-0xta0kqwzkf28.js → turbopack-05qngmxam3ar~.js} +1 -1
  36. package/out/_not-found/__next._full.txt +12 -12
  37. package/out/_not-found/__next._head.txt +3 -3
  38. package/out/_not-found/__next._index.txt +7 -7
  39. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  40. package/out/_not-found/__next._not-found.txt +3 -3
  41. package/out/_not-found/__next._tree.txt +2 -2
  42. package/out/_not-found/index.html +2 -2
  43. package/out/_not-found/index.txt +12 -12
  44. package/out/admin/__next._full.txt +14 -14
  45. package/out/admin/__next._head.txt +3 -3
  46. package/out/admin/__next._index.txt +7 -7
  47. package/out/admin/__next._tree.txt +3 -3
  48. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  49. package/out/admin/__next.admin.txt +4 -4
  50. package/out/admin/index.html +2 -2
  51. package/out/admin/index.txt +14 -14
  52. package/out/app/__next._full.txt +13 -13
  53. package/out/app/__next._head.txt +3 -3
  54. package/out/app/__next._index.txt +7 -7
  55. package/out/app/__next._tree.txt +2 -2
  56. package/out/app/__next.app.__PAGE__.txt +4 -4
  57. package/out/app/__next.app.txt +3 -3
  58. package/out/app/index.html +2 -2
  59. package/out/app/index.txt +13 -13
  60. package/out/chat/__next._full.txt +14 -14
  61. package/out/chat/__next._head.txt +3 -3
  62. package/out/chat/__next._index.txt +7 -7
  63. package/out/chat/__next._tree.txt +3 -3
  64. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  65. package/out/chat/__next.chat.txt +4 -4
  66. package/out/chat/index.html +2 -2
  67. package/out/chat/index.txt +14 -14
  68. package/out/chat/join/__next._full.txt +14 -14
  69. package/out/chat/join/__next._head.txt +3 -3
  70. package/out/chat/join/__next._index.txt +7 -7
  71. package/out/chat/join/__next._tree.txt +3 -3
  72. package/out/chat/join/__next.chat.join.__PAGE__.txt +4 -4
  73. package/out/chat/join/__next.chat.join.txt +3 -3
  74. package/out/chat/join/__next.chat.txt +4 -4
  75. package/out/chat/join/index.html +2 -2
  76. package/out/chat/join/index.txt +14 -14
  77. package/out/download/__next._full.txt +34 -36
  78. package/out/download/__next._head.txt +3 -3
  79. package/out/download/__next._index.txt +7 -7
  80. package/out/download/__next._tree.txt +4 -4
  81. package/out/download/__next.download.__PAGE__.txt +7 -7
  82. package/out/download/__next.download.txt +3 -3
  83. package/out/download/index.html +2 -2
  84. package/out/download/index.txt +34 -36
  85. package/out/favicon.ico +0 -0
  86. package/out/game/__next._full.txt +20 -0
  87. package/out/game/__next._head.txt +5 -0
  88. package/out/game/__next._index.txt +9 -0
  89. package/out/game/__next._tree.txt +5 -0
  90. package/out/game/__next.game.__PAGE__.txt +6 -0
  91. package/out/game/__next.game.txt +5 -0
  92. package/out/game/gandengyan/__next._full.txt +26 -0
  93. package/out/game/gandengyan/__next._head.txt +5 -0
  94. package/out/game/gandengyan/__next._index.txt +9 -0
  95. package/out/game/gandengyan/__next._tree.txt +6 -0
  96. package/out/game/gandengyan/__next.game.gandengyan.__PAGE__.txt +10 -0
  97. package/out/game/gandengyan/__next.game.gandengyan.txt +5 -0
  98. package/out/game/gandengyan/__next.game.txt +5 -0
  99. package/out/game/gandengyan/index.html +15 -0
  100. package/out/game/gandengyan/index.txt +26 -0
  101. package/out/game/index.html +1 -0
  102. package/out/game/index.txt +20 -0
  103. package/out/game/zhajinhua/__next._full.txt +25 -0
  104. package/out/game/zhajinhua/__next._head.txt +5 -0
  105. package/out/game/zhajinhua/__next._index.txt +9 -0
  106. package/out/game/zhajinhua/__next._tree.txt +5 -0
  107. package/out/game/zhajinhua/__next.game.txt +5 -0
  108. package/out/game/zhajinhua/__next.game.zhajinhua.__PAGE__.txt +9 -0
  109. package/out/game/zhajinhua/__next.game.zhajinhua.txt +5 -0
  110. package/out/game/zhajinhua/index.html +15 -0
  111. package/out/game/zhajinhua/index.txt +25 -0
  112. package/out/index.html +2 -2
  113. package/out/index.txt +15 -15
  114. package/out/logo-512.png +0 -0
  115. package/out/logo.ico +0 -0
  116. package/out/logo.svg +12 -0
  117. package/out/note/__next._full.txt +13 -13
  118. package/out/note/__next._head.txt +3 -3
  119. package/out/note/__next._index.txt +7 -7
  120. package/out/note/__next._tree.txt +2 -2
  121. package/out/note/__next.note.__PAGE__.txt +4 -4
  122. package/out/note/__next.note.txt +3 -3
  123. package/out/note/index.html +2 -2
  124. package/out/note/index.txt +13 -13
  125. package/out/ping/__next._full.txt +14 -14
  126. package/out/ping/__next._head.txt +3 -3
  127. package/out/ping/__next._index.txt +7 -7
  128. package/out/ping/__next._tree.txt +3 -3
  129. package/out/ping/__next.ping.__PAGE__.txt +5 -5
  130. package/out/ping/__next.ping.txt +3 -3
  131. package/out/ping/index.html +2 -2
  132. package/out/ping/index.txt +14 -14
  133. package/out/web3/__next._full.txt +13 -13
  134. package/out/web3/__next._head.txt +3 -3
  135. package/out/web3/__next._index.txt +7 -7
  136. package/out/web3/__next._tree.txt +2 -2
  137. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  138. package/out/web3/__next.web3.txt +3 -3
  139. package/out/web3/ed25519/__next._full.txt +11 -11
  140. package/out/web3/ed25519/__next._head.txt +3 -3
  141. package/out/web3/ed25519/__next._index.txt +7 -7
  142. package/out/web3/ed25519/__next._tree.txt +2 -2
  143. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  144. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  145. package/out/web3/ed25519/__next.web3.txt +3 -3
  146. package/out/web3/ed25519/index.html +1 -1
  147. package/out/web3/ed25519/index.txt +11 -11
  148. package/out/web3/index.html +2 -2
  149. package/out/web3/index.txt +13 -13
  150. package/out/web3/tools/__next._full.txt +11 -11
  151. package/out/web3/tools/__next._head.txt +3 -3
  152. package/out/web3/tools/__next._index.txt +7 -7
  153. package/out/web3/tools/__next._tree.txt +2 -2
  154. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  155. package/out/web3/tools/__next.web3.tools.txt +3 -3
  156. package/out/web3/tools/__next.web3.txt +3 -3
  157. package/out/web3/tools/index.html +1 -1
  158. package/out/web3/tools/index.txt +11 -11
  159. package/package.json +6 -5
  160. package/public/favicon.ico +0 -0
  161. package/public/logo-512.png +0 -0
  162. package/public/logo.ico +0 -0
  163. package/public/logo.svg +12 -0
  164. package/server/index.js +0 -8
  165. package/server/src/config.js +1 -1
  166. package/server/src/core/gameRoom.js +222 -0
  167. package/server/src/core/zhajinhua.js +563 -0
  168. package/server/src/games/gandengyan.js +354 -413
  169. package/server/src/http/app.js +22 -14
  170. package/server/src/http/uploads.js +1 -0
  171. package/server/src/index.js +275 -90
  172. package/server/src/utils/avatar.js +3 -2
  173. package/out/_next/static/chunks/04mo7rr..0_1q.js +0 -1
  174. package/out/_next/static/chunks/06rf3qq5ggs6v.js +0 -1
  175. package/out/_next/static/chunks/07td.jq7xff84.css +0 -1
  176. package/out/_next/static/chunks/0_0oph_z1az14.js +0 -1
  177. package/out/_next/static/chunks/0ao1lbi4b.sfa.js +0 -1
  178. package/out/_next/static/chunks/0cl7d~7abnk_p.css +0 -1
  179. package/out/_next/static/chunks/0d306t1wvjpdx.js +0 -1
  180. package/out/_next/static/chunks/0g_a~e050bgzg.css +0 -1
  181. package/out/_next/static/chunks/0m_5nb6x8qy._.js +0 -1
  182. package/out/_next/static/chunks/0n.ayxmsar6e5.js +0 -1
  183. package/out/_next/static/chunks/0olqjomda37-e.js +0 -1
  184. package/out/_next/static/chunks/0qgx9t4jx16ua.css +0 -1
  185. package/out/_next/static/chunks/0s~g.l~x049o2.js +0 -1
  186. package/out/_next/static/chunks/0voe1.ttrh84k.css +0 -1
  187. package/out/_next/static/chunks/0wtf0xsiicxx6.js +0 -1
  188. package/out/_next/static/chunks/0x.ky97owcxxs.js +0 -1
  189. package/out/_next/static/chunks/0ysj5b94vu4ri.js +0 -1
  190. package/out/_next/static/chunks/153-sz7s.qml2.js +0 -1
  191. package/out/_next/static/chunks/17cwkb2yn_akx.js +0 -1
  192. package/out/_next/static/chunks/184hxsuf-5c84.js +0 -1
  193. package/out/gandengyan/__next._full.txt +0 -25
  194. package/out/gandengyan/__next._head.txt +0 -5
  195. package/out/gandengyan/__next._index.txt +0 -9
  196. package/out/gandengyan/__next._tree.txt +0 -5
  197. package/out/gandengyan/__next.gandengyan.__PAGE__.txt +0 -10
  198. package/out/gandengyan/__next.gandengyan.txt +0 -5
  199. package/out/gandengyan/index.html +0 -15
  200. package/out/gandengyan/index.txt +0 -25
  201. /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → 2smv1H9Y4Z2Ri-SL-UFgR}/_buildManifest.js +0 -0
  202. /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → 2smv1H9Y4Z2Ri-SL-UFgR}/_clientMiddlewareManifest.js +0 -0
  203. /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → 2smv1H9Y4Z2Ri-SL-UFgR}/_ssgManifest.js +0 -0
  204. /package/out/{pwa-512x512.png → avatar.png} +0 -0
  205. /package/public/{pwa-512x512.png → avatar.png} +0 -0
@@ -1,173 +1,40 @@
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')
6
4
  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
-
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 })
18
- }
19
-
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
- }
7
+ const INITIAL_SCORE = 1000
8
+
9
+ export function createGanDengYanRoom({
10
+ roomCode,
11
+ ownerAddress,
12
+ ownerName,
13
+ players = [],
14
+ random = Math.random,
15
+ }) {
16
+ const roomPlayers = normalizePlayers(players)
17
+ if (!roomPlayers.some(player => player.address === normalizeAddress(ownerAddress))) {
18
+ roomPlayers.unshift({
19
+ address: normalizeAddress(ownerAddress),
20
+ name: cleanName(ownerName),
21
+ })
121
22
  }
122
23
 
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
24
  const room = {
154
- id: roomId,
155
- ownerId,
25
+ id: String(roomCode || '').toUpperCase(),
26
+ ownerAddress: normalizeAddress(ownerAddress),
156
27
  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
- ],
28
+ seq: 1,
29
+ players: roomPlayers.slice(0, 6).map((player, seat) => ({
30
+ address: player.address,
31
+ name: cleanName(player.name),
32
+ seat,
33
+ hand: [],
34
+ handCount: 0,
35
+ score: INITIAL_SCORE,
36
+ playedCards: 0,
37
+ })),
171
38
  deck: [],
172
39
  discard: [],
173
40
  table: null,
@@ -181,185 +48,230 @@ function createRoom({ roomId, ownerId, ownerName, ownerAddress }) {
181
48
  roundResult: null,
182
49
  log: ['房间已创建'],
183
50
  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)
51
+ random,
52
+ }
53
+ return publicGanDengYanRoom(room)
54
+ }
55
+
56
+ export function syncGanDengYanLobby(room, players = []) {
57
+ const state = hydrateGanDengYanRoom(room)
58
+ if (!state || state.status !== 'lobby') return state
59
+ const currentScores = new Map(state.players.map(player => [player.address, player.score]))
60
+ state.players = normalizePlayers(players)
61
+ .slice(0, 6)
62
+ .map((player, seat) => ({
63
+ address: player.address,
64
+ name: cleanName(player.name),
65
+ seat,
66
+ hand: [],
67
+ handCount: 0,
68
+ score: currentScores.get(player.address) ?? INITIAL_SCORE,
69
+ playedCards: 0,
70
+ }))
71
+ state.seq += 1
72
+ return publicGanDengYanRoom(state)
73
+ }
74
+
75
+ export function startGanDengYanRound(room, random = Math.random) {
76
+ const state = hydrateGanDengYanRoom(room)
77
+ if (!state || state.players.length < 2) {
78
+ throw new Error('至少需要 2 名玩家')
79
+ }
80
+
81
+ state.deck = shuffle(createDeck(), random)
82
+ state.discard = []
83
+ state.table = null
84
+ state.passSeats = []
85
+ state.winnerSeat = null
86
+ state.roundResult = null
87
+ state.baseScore = 1
88
+ state.bombCount = 0
89
+ state.status = 'playing'
90
+ for (const player of orderedPlayers(state)) {
91
+ player.hand = draw(state, INITIAL_HAND_SIZE)
92
+ player.handCount = player.hand.length
257
93
  player.playedCards = 0
258
94
  sortHand(player.hand)
259
95
  }
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(',')}`)
96
+ const starter = chooseStarter(state, random)
97
+ state.currentSeat = starter.seat
98
+ state.lastWinnerSeat = null
99
+ state.log = [`新一局开始,${starter.name} 先出牌`]
100
+ if (state.diceRolls.length > 0) {
101
+ state.log.unshift(
102
+ `骰子结果:${state.diceRolls
103
+ .map(roll => `${roll.name} ${roll.value}`)
104
+ .join(',')}`
105
+ )
266
106
  }
267
- room.updatedAt = Date.now()
107
+ state.seq += 1
108
+ return publicGanDengYanRoom(state)
268
109
  }
269
110
 
270
- function playCards(room, playerId, cardIds) {
271
- const player = currentPlayer(room)
272
- if (!player || player.id !== playerId) throw new Error('还没轮到你')
111
+ export function playGanDengYanCards(room, address, cardIds) {
112
+ const state = hydrateGanDengYanRoom(room)
113
+ const player = currentPlayer(state)
114
+ const normalizedAddress = normalizeAddress(address)
115
+ if (!player || player.address !== normalizedAddress) {
116
+ return { ok: false, error: '还没轮到你', state: publicGanDengYanRoom(state) }
117
+ }
118
+
273
119
  const cards = cardIds.map(id => player.hand.find(card => card.id === id))
274
- if (cards.length === 0 || cards.some(card => !card)) throw new Error('手牌不存在')
120
+ if (cards.length === 0 || cards.some(card => !card)) {
121
+ return { ok: false, error: '手牌不存在', state: publicGanDengYanRoom(state) }
122
+ }
275
123
  const combo = analyzeCards(cards)
276
- if (!combo) throw new Error('这个牌型不合法')
277
- if (!canBeat(combo, room.table?.combo)) throw new Error('出的牌压不过上一手')
124
+ if (!combo) {
125
+ return { ok: false, error: '这个牌型不合法', state: publicGanDengYanRoom(state) }
126
+ }
127
+ if (!canBeat(combo, state.table?.combo)) {
128
+ return { ok: false, error: '出的牌压不过上一手', state: publicGanDengYanRoom(state) }
129
+ }
130
+
278
131
  player.hand = player.hand.filter(card => !cardIds.includes(card.id))
132
+ player.handCount = player.hand.length
279
133
  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
134
+ state.discard.push(...cards)
135
+ state.table = { seat: player.seat, playerName: player.name, cards, combo }
136
+ state.passSeats = []
137
+ state.lastWinnerSeat = player.seat
284
138
  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}` : ''}`)
139
+ state.bombCount += 1
140
+ state.baseScore *= 2
141
+ }
142
+ state.log.unshift(
143
+ `${player.name} 出 ${combo.label} ${cards.map(labelCard).join(' ')}${
144
+ combo.type === 'bomb' ? `,底分 ${state.baseScore}` : ''
145
+ }`
146
+ )
289
147
  if (player.hand.length === 0) {
290
- finishGame(room, player)
148
+ finishGame(state, player)
291
149
  } 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)
150
+ advanceTurn(state)
313
151
  }
314
- room.updatedAt = Date.now()
152
+ state.seq += 1
153
+ return { ok: true, state: publicGanDengYanRoom(state) }
315
154
  }
316
155
 
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))
156
+ export function passGanDengYanTurn(room, address) {
157
+ const state = hydrateGanDengYanRoom(room)
158
+ const player = currentPlayer(state)
159
+ const normalizedAddress = normalizeAddress(address)
160
+ if (!player || player.address !== normalizedAddress) {
161
+ return { ok: false, error: '还没轮到你', state: publicGanDengYanRoom(state) }
162
+ }
163
+ if (!state.table) {
164
+ return { ok: false, error: '领出时不能不要', state: publicGanDengYanRoom(state) }
165
+ }
166
+
167
+ if (!state.passSeats.includes(player.seat)) state.passSeats.push(player.seat)
168
+ state.log.unshift(`${player.name} 不要`)
169
+ const activeSeats = activePlayers(state).map(item => item.seat)
170
+ const seatsToBeat = activeSeats.filter(seat => seat !== state.lastWinnerSeat)
171
+ if (seatsToBeat.every(seat => state.passSeats.includes(seat))) {
172
+ refillAfterRound(state)
173
+ state.currentSeat = state.lastWinnerSeat
174
+ state.table = null
175
+ state.passSeats = []
176
+ state.log.unshift('本轮结束,所有玩家各补 1 张,重新领出')
323
177
  } else {
324
- passTurn(room, player.id)
178
+ advanceTurn(state)
325
179
  }
326
- return true
180
+ state.seq += 1
181
+ return { ok: true, state: publicGanDengYanRoom(state) }
327
182
  }
328
183
 
329
- function publicRoom(room, viewerId) {
184
+ export function publicGanDengYanRoom(room) {
185
+ if (!room) return null
330
186
  return {
331
187
  id: room.id,
332
- ownerId: room.ownerId,
188
+ ownerAddress: room.ownerAddress,
333
189
  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),
190
+ seq: Number(room.seq || 1),
191
+ deckCount: room.deck?.length || Number(room.deckCount || 0),
192
+ discardCount: room.discard?.length || Number(room.discardCount || 0),
193
+ currentSeat: Number(room.currentSeat || 0),
194
+ lastWinnerSeat:
195
+ room.lastWinnerSeat === null || room.lastWinnerSeat === undefined
196
+ ? null
197
+ : Number(room.lastWinnerSeat),
198
+ previousWinnerSeat:
199
+ room.previousWinnerSeat === null || room.previousWinnerSeat === undefined
200
+ ? null
201
+ : Number(room.previousWinnerSeat),
202
+ baseScore: Number(room.baseScore || 1),
203
+ bombCount: Number(room.bombCount || 0),
204
+ diceRolls: Array.isArray(room.diceRolls) ? room.diceRolls : [],
205
+ roundResult: room.roundResult || null,
206
+ table: room.table
207
+ ? { ...room.table, cards: room.table.cards.map(publicCard) }
208
+ : null,
209
+ passSeats: Array.isArray(room.passSeats) ? room.passSeats : [],
210
+ winnerSeat:
211
+ room.winnerSeat === null || room.winnerSeat === undefined
212
+ ? null
213
+ : Number(room.winnerSeat),
214
+ log: Array.isArray(room.log) ? room.log.slice(0, 18) : [],
348
215
  players: orderedPlayers(room).map(player => ({
349
- id: player.id,
350
216
  address: player.address,
351
217
  name: player.name,
352
- bot: player.bot,
353
218
  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) : [],
219
+ handCount: player.hand?.length ?? player.handCount ?? 0,
220
+ score: Number(player.score ?? INITIAL_SCORE),
221
+ playedCards: Number(player.playedCards || 0),
222
+ hand: Array.isArray(player.hand) ? player.hand.map(publicCard) : [],
359
223
  })),
360
224
  }
361
225
  }
362
226
 
227
+ export function hydrateGanDengYanRoom(input) {
228
+ if (!input || typeof input !== 'object') return null
229
+ return {
230
+ id: String(input.id || '').toUpperCase(),
231
+ ownerAddress: normalizeAddress(input.ownerAddress),
232
+ status:
233
+ input.status === 'playing' || input.status === 'finished'
234
+ ? input.status
235
+ : 'lobby',
236
+ seq: Number(input.seq || 1),
237
+ players: Array.isArray(input.players)
238
+ ? input.players.map(normalizeRoundPlayer).filter(Boolean)
239
+ : [],
240
+ deck: Array.isArray(input.deck) ? input.deck.map(normalizeCard).filter(Boolean) : [],
241
+ discard: Array.isArray(input.discard)
242
+ ? input.discard.map(normalizeCard).filter(Boolean)
243
+ : [],
244
+ table:
245
+ input.table && typeof input.table === 'object'
246
+ ? {
247
+ ...input.table,
248
+ cards: Array.isArray(input.table.cards)
249
+ ? input.table.cards.map(normalizeCard).filter(Boolean)
250
+ : [],
251
+ }
252
+ : null,
253
+ currentSeat: Number(input.currentSeat || 0),
254
+ lastWinnerSeat:
255
+ input.lastWinnerSeat === null || input.lastWinnerSeat === undefined
256
+ ? null
257
+ : Number(input.lastWinnerSeat),
258
+ previousWinnerSeat:
259
+ input.previousWinnerSeat === null || input.previousWinnerSeat === undefined
260
+ ? null
261
+ : Number(input.previousWinnerSeat),
262
+ passSeats: Array.isArray(input.passSeats) ? input.passSeats.map(Number) : [],
263
+ baseScore: Number(input.baseScore || 1),
264
+ bombCount: Number(input.bombCount || 0),
265
+ diceRolls: Array.isArray(input.diceRolls) ? input.diceRolls : [],
266
+ roundResult: input.roundResult || null,
267
+ log: Array.isArray(input.log) ? input.log.map(String) : [],
268
+ winnerSeat:
269
+ input.winnerSeat === null || input.winnerSeat === undefined
270
+ ? null
271
+ : Number(input.winnerSeat),
272
+ }
273
+ }
274
+
363
275
  export function analyzeCards(cards) {
364
276
  if (!cards?.length) return null
365
277
  const jokerCount = cards.filter(isJoker).length
@@ -367,7 +279,9 @@ export function analyzeCards(cards) {
367
279
  if (normals.length === 0) return null
368
280
  const bomb = analyzeBomb(cards, normals, jokerCount)
369
281
  if (bomb) return bomb
370
- if (cards.length === 1 && jokerCount === 0) return makeCombo('single', cardValue(cards[0]), 1, [cardValue(cards[0])])
282
+ if (cards.length === 1 && jokerCount === 0) {
283
+ return makeCombo('single', cardValue(cards[0]), 1, [cardValue(cards[0])])
284
+ }
371
285
  if (cards.length === 2 && canRepresentSameRank(normals, jokerCount)) {
372
286
  const value = sameRankValue(normals)
373
287
  return makeCombo('pair', value, 2, [value, value])
@@ -378,14 +292,26 @@ export function analyzeCards(cards) {
378
292
  function analyzeBomb(cards, normals, jokerCount) {
379
293
  if (cards.length < 3 || !canRepresentSameRank(normals, jokerCount)) return null
380
294
  const value = sameRankValue(normals)
381
- return makeCombo('bomb', value, cards.length, Array(cards.length).fill(value), jokerCount === 0)
295
+ return makeCombo(
296
+ 'bomb',
297
+ value,
298
+ cards.length,
299
+ Array(cards.length).fill(value),
300
+ jokerCount === 0
301
+ )
382
302
  }
383
303
 
384
304
  function analyzeStraight(cards) {
385
305
  if (cards.length < 3) return null
386
306
  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])) {
307
+ const values = STRAIGHT_RANKS.slice(start, start + cards.length).map(rank =>
308
+ RANK_VALUE.get(rank)
309
+ )
310
+ if (
311
+ cards.every(
312
+ (card, index) => isJoker(card) || cardValue(card) === values[index]
313
+ )
314
+ ) {
389
315
  return makeCombo('straight', values.at(-1), cards.length, values)
390
316
  }
391
317
  }
@@ -396,9 +322,15 @@ function analyzePairStraight(cards) {
396
322
  if (cards.length < 4 || cards.length % 2 !== 0) return null
397
323
  const pairCount = cards.length / 2
398
324
  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))
325
+ const pairValues = STRAIGHT_RANKS.slice(start, start + pairCount).map(rank =>
326
+ RANK_VALUE.get(rank)
327
+ )
400
328
  const values = pairValues.flatMap(value => [value, value])
401
- if (cards.every((card, index) => isJoker(card) || cardValue(card) === values[index])) {
329
+ if (
330
+ cards.every(
331
+ (card, index) => isJoker(card) || cardValue(card) === values[index]
332
+ )
333
+ ) {
402
334
  return makeCombo('pairStraight', pairValues.at(-1), cards.length, values)
403
335
  }
404
336
  }
@@ -406,7 +338,14 @@ function analyzePairStraight(cards) {
406
338
  }
407
339
 
408
340
  function makeCombo(type, value, length, resolvedValues, pure = true) {
409
- return { type, value, length, resolvedValues, pure, label: labelCombo({ type, value, length, resolvedValues, pure }) }
341
+ return {
342
+ type,
343
+ value,
344
+ length,
345
+ resolvedValues,
346
+ pure,
347
+ label: labelCombo({ type, value, length, resolvedValues, pure }),
348
+ }
410
349
  }
411
350
 
412
351
  function canBeat(combo, tableCombo) {
@@ -421,39 +360,59 @@ function canBeat(combo, tableCombo) {
421
360
  }
422
361
  if (combo.length !== tableCombo.length) return false
423
362
  if (combo.type === 'single' || combo.type === 'pair') {
424
- return combo.value === nextValue(tableCombo.value) || combo.value === RANK_VALUE.get('2')
363
+ return (
364
+ combo.value === nextValue(tableCombo.value) ||
365
+ (combo.value === RANK_VALUE.get('2') && tableCombo.value !== RANK_VALUE.get('2'))
366
+ )
425
367
  }
426
368
  return combo.value === nextValue(tableCombo.value)
427
369
  }
428
370
 
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) {
371
+ function normalizePlayers(players) {
372
+ const seen = new Set()
373
+ return players
374
+ .map(player => ({
375
+ address: normalizeAddress(player.address),
376
+ name: cleanName(player.name),
377
+ publicKey: String(player.publicKey || ''),
378
+ }))
379
+ .filter(player => {
380
+ if (!player.address || seen.has(player.address)) return false
381
+ seen.add(player.address)
382
+ return true
383
+ })
384
+ }
385
+
386
+ function normalizeRoundPlayer(input) {
387
+ const address = normalizeAddress(input.address)
388
+ if (!address) return null
389
+ return {
390
+ address,
391
+ name: cleanName(input.name),
392
+ seat: Number(input.seat || 0),
393
+ hand: Array.isArray(input.hand) ? input.hand.map(normalizeCard).filter(Boolean) : [],
394
+ handCount: Number(input.handCount || 0),
395
+ score: Number(input.score ?? INITIAL_SCORE),
396
+ playedCards: Number(input.playedCards || 0),
397
+ }
398
+ }
399
+
400
+ function normalizeCard(card) {
401
+ if (!card || typeof card !== 'object') return null
402
+ const rank = String(card.rank || '')
403
+ const suit = String(card.suit || '')
404
+ if (!RANK_VALUE.has(rank) && rank !== 'SJ' && rank !== 'BJ') return null
405
+ if (!SUITS.includes(suit) && suit !== 'Joker') return null
443
406
  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,
407
+ id: String(card.id || `${suit}-${rank}`),
408
+ suit,
409
+ rank,
410
+ label: card.label || labelCard({ suit, rank }),
411
+ color: card.color || cardColor({ suit, rank }),
453
412
  }
454
413
  }
455
414
 
456
- function chooseStarter(room) {
415
+ function chooseStarter(room, random) {
457
416
  if (room.previousWinnerSeat !== null) {
458
417
  room.diceRolls = []
459
418
  return orderedPlayers(room).find(player => player.seat === room.previousWinnerSeat) || orderedPlayers(room)[0]
@@ -461,14 +420,20 @@ function chooseStarter(room) {
461
420
  let candidates = orderedPlayers(room)
462
421
  let rolls = []
463
422
  while (candidates.length > 1) {
464
- rolls = candidates.map(player => ({ seat: player.seat, name: player.name, value: rollDice() }))
423
+ rolls = candidates.map(player => ({
424
+ seat: player.seat,
425
+ name: player.name,
426
+ value: rollDice(random),
427
+ }))
465
428
  const max = Math.max(...rolls.map(roll => roll.value))
466
429
  const winners = rolls.filter(roll => roll.value === max)
467
430
  if (winners.length === 1) {
468
431
  room.diceRolls = rolls
469
432
  return candidates.find(player => player.seat === winners[0].seat)
470
433
  }
471
- candidates = candidates.filter(player => winners.some(roll => roll.seat === player.seat))
434
+ candidates = candidates.filter(player =>
435
+ winners.some(roll => roll.seat === player.seat)
436
+ )
472
437
  }
473
438
  room.diceRolls = rolls
474
439
  return candidates[0]
@@ -486,10 +451,23 @@ function finishGame(room, winner) {
486
451
  const loss = sealed ? SEALED_PENALTY : player.hand.length * room.baseScore
487
452
  player.score -= loss
488
453
  winnerGain += loss
489
- losers.push({ seat: player.seat, name: player.name, loss, sealed, cardsLeft: player.hand.length })
454
+ losers.push({
455
+ seat: player.seat,
456
+ name: player.name,
457
+ loss,
458
+ sealed,
459
+ cardsLeft: player.hand.length,
460
+ })
490
461
  }
491
462
  winner.score += winnerGain
492
- room.roundResult = { winnerSeat: winner.seat, winnerName: winner.name, winnerGain, baseScore: room.baseScore, bombCount: room.bombCount, losers }
463
+ room.roundResult = {
464
+ winnerSeat: winner.seat,
465
+ winnerName: winner.name,
466
+ winnerGain,
467
+ baseScore: room.baseScore,
468
+ bombCount: room.bombCount,
469
+ losers,
470
+ }
493
471
  room.log.unshift(`${winner.name} 获胜,赢 ${winnerGain} 分`)
494
472
  }
495
473
 
@@ -497,34 +475,27 @@ function refillAfterRound(room) {
497
475
  for (const player of activePlayers(room)) {
498
476
  if (room.deck.length === 0) break
499
477
  player.hand.push(...draw(room, 1))
478
+ player.handCount = player.hand.length
500
479
  sortHand(player.hand)
501
480
  }
502
481
  }
503
482
 
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) {
483
+ function createDeck() {
513
484
  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 })
485
+ for (const suit of SUITS) {
486
+ for (const rank of RANKS) {
487
+ cards.push({ id: `${suit}-${rank}`, suit, rank })
517
488
  }
518
- cards.push({ id: `${deck}-SJ`, suit: 'Joker', rank: 'SJ', deck })
519
- cards.push({ id: `${deck}-BJ`, suit: 'Joker', rank: 'BJ', deck })
520
489
  }
521
- return cards
490
+ cards.push({ id: 'SJ', suit: 'Joker', rank: 'SJ' })
491
+ cards.push({ id: 'BJ', suit: 'Joker', rank: 'BJ' })
492
+ return cards.map(publicCard)
522
493
  }
523
494
 
524
- function shuffle(cards) {
495
+ function shuffle(cards, random) {
525
496
  const copy = [...cards]
526
497
  for (let index = copy.length - 1; index > 0; index -= 1) {
527
- const swapIndex = Math.floor(Math.random() * (index + 1))
498
+ const swapIndex = Math.floor(random() * (index + 1))
528
499
  ;[copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]]
529
500
  }
530
501
  return copy
@@ -547,7 +518,7 @@ function activePlayers(room) {
547
518
  }
548
519
 
549
520
  function orderedPlayers(room) {
550
- return [...room.players].sort((a, b) => a.seat - b.seat)
521
+ return [...(room?.players || [])].sort((a, b) => a.seat - b.seat)
551
522
  }
552
523
 
553
524
  function advanceTurn(room) {
@@ -556,40 +527,7 @@ function advanceTurn(room) {
556
527
  room.currentSeat = seats[(currentIndex + 1) % seats.length]
557
528
  }
558
529
 
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) {
530
+ function canRepresentSameRank(normals, jokerCount = 0) {
593
531
  if (normals.length === 0) return false
594
532
  const first = normals[0].rank
595
533
  return normals.every(card => card.rank === first) && normals.length + jokerCount >= 2
@@ -616,7 +554,13 @@ function suitValue(suit) {
616
554
  }
617
555
 
618
556
  function publicCard(card) {
619
- return { id: card.id, suit: card.suit, rank: card.rank, label: labelCard(card), color: cardColor(card) }
557
+ return {
558
+ id: card.id,
559
+ suit: card.suit,
560
+ rank: card.rank,
561
+ label: labelCard(card),
562
+ color: cardColor(card),
563
+ }
620
564
  }
621
565
 
622
566
  function labelCard(card) {
@@ -630,11 +574,15 @@ function suitSymbol(suit) {
630
574
  }
631
575
 
632
576
  function cardColor(card) {
633
- return card.suit === 'H' || card.suit === 'D' || card.rank === 'BJ' ? 'red' : 'black'
577
+ return card.suit === 'H' || card.suit === 'D' || card.rank === 'BJ'
578
+ ? 'red'
579
+ : 'black'
634
580
  }
635
581
 
636
582
  function labelCombo(combo) {
637
- const resolved = combo.resolvedValues?.length ? `(${combo.resolvedValues.map(valueLabel).join(' ')})` : ''
583
+ const resolved = combo.resolvedValues?.length
584
+ ? `(${combo.resolvedValues.map(valueLabel).join(' ')})`
585
+ : ''
638
586
  return `${{
639
587
  single: '单张',
640
588
  pair: '对子',
@@ -654,22 +602,15 @@ function nextValue(value) {
654
602
  return RANK_VALUE.get(RANKS[index + 1])
655
603
  }
656
604
 
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) || '玩家'
605
+ function rollDice(random) {
606
+ return Math.floor(random() * 6) + 1
667
607
  }
668
608
 
669
- function rollDice() {
670
- return Math.floor(Math.random() * 6) + 1
609
+ function normalizeAddress(value) {
610
+ const address = String(value || '').trim()
611
+ return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''
671
612
  }
672
613
 
673
- function clamp(value, min, max) {
674
- return Math.max(min, Math.min(max, value))
614
+ function cleanName(name) {
615
+ return String(name || '玩家').trim().slice(0, 16) || '玩家'
675
616
  }