most-box 0.1.2 → 0.1.3

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 (145) hide show
  1. package/README.md +18 -5
  2. package/electron/afterPack.cjs +87 -0
  3. package/electron/main.js +85 -1
  4. package/electron/updateChecker.js +97 -0
  5. package/electron/updateChecker.test.js +147 -0
  6. package/out/404/index.html +2 -2
  7. package/out/404.html +2 -2
  8. package/out/__next.__PAGE__.txt +4 -4
  9. package/out/__next._full.txt +12 -12
  10. package/out/__next._head.txt +3 -3
  11. package/out/__next._index.txt +6 -6
  12. package/out/__next._tree.txt +1 -1
  13. package/out/_next/static/chunks/04mo7rr..0_1q.js +1 -0
  14. package/out/_next/static/chunks/06rf3qq5ggs6v.js +1 -0
  15. package/out/_next/static/chunks/{0aq.rc9woa2nz.js → 0_0oph_z1az14.js} +1 -1
  16. package/out/_next/static/chunks/0cl7d~7abnk_p.css +1 -0
  17. package/out/_next/static/chunks/0d306t1wvjpdx.js +1 -0
  18. package/out/_next/static/chunks/{0etes81d_cihn.js → 0m_5nb6x8qy._.js} +1 -1
  19. package/out/_next/static/chunks/0n.ayxmsar6e5.js +1 -0
  20. package/out/_next/static/chunks/0olqjomda37-e.js +1 -0
  21. package/out/_next/static/chunks/{16xls5tt_68lx.js → 0s~g.l~x049o2.js} +1 -1
  22. package/out/_next/static/chunks/0voe1.ttrh84k.css +1 -0
  23. package/out/_next/static/chunks/0x.ky97owcxxs.js +1 -0
  24. package/out/_next/static/chunks/0ysj5b94vu4ri.js +1 -0
  25. package/out/_next/static/chunks/{0q0ksgxg98xgd.js → 17cwkb2yn_akx.js} +1 -1
  26. package/out/_next/static/chunks/184hxsuf-5c84.js +1 -0
  27. package/out/_not-found/__next._full.txt +11 -11
  28. package/out/_not-found/__next._head.txt +3 -3
  29. package/out/_not-found/__next._index.txt +6 -6
  30. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  31. package/out/_not-found/__next._not-found.txt +3 -3
  32. package/out/_not-found/__next._tree.txt +1 -1
  33. package/out/_not-found/index.html +2 -2
  34. package/out/_not-found/index.txt +11 -11
  35. package/out/admin/__next._full.txt +12 -12
  36. package/out/admin/__next._head.txt +3 -3
  37. package/out/admin/__next._index.txt +6 -6
  38. package/out/admin/__next._tree.txt +1 -1
  39. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  40. package/out/admin/__next.admin.txt +3 -3
  41. package/out/admin/index.html +2 -2
  42. package/out/admin/index.txt +12 -12
  43. package/out/app/__next._full.txt +12 -12
  44. package/out/app/__next._head.txt +3 -3
  45. package/out/app/__next._index.txt +6 -6
  46. package/out/app/__next._tree.txt +1 -1
  47. package/out/app/__next.app.__PAGE__.txt +4 -4
  48. package/out/app/__next.app.txt +3 -3
  49. package/out/app/index.html +2 -2
  50. package/out/app/index.txt +12 -12
  51. package/out/chat/__next._full.txt +12 -12
  52. package/out/chat/__next._head.txt +3 -3
  53. package/out/chat/__next._index.txt +6 -6
  54. package/out/chat/__next._tree.txt +1 -1
  55. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  56. package/out/chat/__next.chat.txt +3 -3
  57. package/out/chat/index.html +2 -2
  58. package/out/chat/index.txt +12 -12
  59. package/out/chat/join/__next._full.txt +12 -12
  60. package/out/chat/join/__next._head.txt +3 -3
  61. package/out/chat/join/__next._index.txt +6 -6
  62. package/out/chat/join/__next._tree.txt +1 -1
  63. package/out/chat/join/__next.chat.join.__PAGE__.txt +4 -4
  64. package/out/chat/join/__next.chat.join.txt +3 -3
  65. package/out/chat/join/__next.chat.txt +3 -3
  66. package/out/chat/join/index.html +2 -2
  67. package/out/chat/join/index.txt +12 -12
  68. package/out/download/__next._full.txt +34 -30
  69. package/out/download/__next._head.txt +3 -3
  70. package/out/download/__next._index.txt +6 -6
  71. package/out/download/__next._tree.txt +2 -2
  72. package/out/download/__next.download.__PAGE__.txt +8 -13
  73. package/out/download/__next.download.txt +3 -3
  74. package/out/download/index.html +2 -2
  75. package/out/download/index.txt +34 -30
  76. package/out/favicon.ico +0 -0
  77. package/out/gandengyan/__next._full.txt +25 -0
  78. package/out/gandengyan/__next._head.txt +5 -0
  79. package/out/gandengyan/__next._index.txt +9 -0
  80. package/out/gandengyan/__next._tree.txt +5 -0
  81. package/out/gandengyan/__next.gandengyan.__PAGE__.txt +10 -0
  82. package/out/gandengyan/__next.gandengyan.txt +5 -0
  83. package/out/gandengyan/index.html +15 -0
  84. package/out/gandengyan/index.txt +25 -0
  85. package/out/index.html +2 -2
  86. package/out/index.txt +12 -12
  87. package/out/note/__next._full.txt +12 -12
  88. package/out/note/__next._head.txt +3 -3
  89. package/out/note/__next._index.txt +6 -6
  90. package/out/note/__next._tree.txt +1 -1
  91. package/out/note/__next.note.__PAGE__.txt +4 -4
  92. package/out/note/__next.note.txt +3 -3
  93. package/out/note/index.html +2 -2
  94. package/out/note/index.txt +12 -12
  95. package/out/ping/__next._full.txt +12 -12
  96. package/out/ping/__next._head.txt +3 -3
  97. package/out/ping/__next._index.txt +6 -6
  98. package/out/ping/__next._tree.txt +1 -1
  99. package/out/ping/__next.ping.__PAGE__.txt +4 -4
  100. package/out/ping/__next.ping.txt +3 -3
  101. package/out/ping/index.html +2 -2
  102. package/out/ping/index.txt +12 -12
  103. package/out/web3/__next._full.txt +12 -12
  104. package/out/web3/__next._head.txt +3 -3
  105. package/out/web3/__next._index.txt +6 -6
  106. package/out/web3/__next._tree.txt +1 -1
  107. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  108. package/out/web3/__next.web3.txt +3 -3
  109. package/out/web3/ed25519/__next._full.txt +10 -10
  110. package/out/web3/ed25519/__next._head.txt +3 -3
  111. package/out/web3/ed25519/__next._index.txt +6 -6
  112. package/out/web3/ed25519/__next._tree.txt +1 -1
  113. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  114. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  115. package/out/web3/ed25519/__next.web3.txt +3 -3
  116. package/out/web3/ed25519/index.html +1 -1
  117. package/out/web3/ed25519/index.txt +10 -10
  118. package/out/web3/index.html +2 -2
  119. package/out/web3/index.txt +12 -12
  120. package/out/web3/tools/__next._full.txt +10 -10
  121. package/out/web3/tools/__next._head.txt +3 -3
  122. package/out/web3/tools/__next._index.txt +6 -6
  123. package/out/web3/tools/__next._tree.txt +1 -1
  124. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  125. package/out/web3/tools/__next.web3.tools.txt +3 -3
  126. package/out/web3/tools/__next.web3.txt +3 -3
  127. package/out/web3/tools/index.html +1 -1
  128. package/out/web3/tools/index.txt +10 -10
  129. package/package.json +28 -29
  130. package/public/favicon.ico +0 -0
  131. package/server/index.js +8 -0
  132. package/server/src/games/gandengyan.js +675 -0
  133. package/server/src/http/access.js +4 -0
  134. package/server/src/http/app.js +7 -0
  135. package/server/src/utils/api.js +19 -1
  136. package/out/_next/static/chunks/0.e2avjgna_b2.js +0 -1
  137. package/out/_next/static/chunks/03h~nhgj0hv3p.css +0 -1
  138. package/out/_next/static/chunks/0gwian.hp3-92.js +0 -1
  139. package/out/_next/static/chunks/0l5_.uqb-uqb8.js +0 -1
  140. package/out/_next/static/chunks/0mex8svsiv-2l.js +0 -1
  141. package/out/_next/static/chunks/0myq9gs8szydh.js +0 -1
  142. package/out/_next/static/chunks/0p0sv~fuddvgr.js +0 -1
  143. /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → aPEZ4zaaR5W3WpSZ0dFsa}/_buildManifest.js +0 -0
  144. /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → aPEZ4zaaR5W3WpSZ0dFsa}/_clientMiddlewareManifest.js +0 -0
  145. /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → aPEZ4zaaR5W3WpSZ0dFsa}/_ssgManifest.js +0 -0
@@ -0,0 +1,675 @@
1
+ import crypto from 'node:crypto'
2
+
3
+ const SUITS = ['S', 'H', 'C', 'D']
4
+ const RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2']
5
+ const STRAIGHT_RANKS = RANKS.filter(rank => rank !== '2')
6
+ const RANK_VALUE = new Map(RANKS.map((rank, index) => [rank, index + 3]))
7
+ const INITIAL_HAND_SIZE = 5
8
+ 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
+ }
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
+ const room = {
154
+ id: roomId,
155
+ ownerId,
156
+ 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
+ ],
171
+ deck: [],
172
+ discard: [],
173
+ table: null,
174
+ currentSeat: 0,
175
+ lastWinnerSeat: null,
176
+ previousWinnerSeat: null,
177
+ passSeats: [],
178
+ baseScore: 1,
179
+ bombCount: 0,
180
+ diceRolls: [],
181
+ roundResult: null,
182
+ log: ['房间已创建'],
183
+ 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)
257
+ player.playedCards = 0
258
+ sortHand(player.hand)
259
+ }
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(',')}`)
266
+ }
267
+ room.updatedAt = Date.now()
268
+ }
269
+
270
+ function playCards(room, playerId, cardIds) {
271
+ const player = currentPlayer(room)
272
+ if (!player || player.id !== playerId) throw new Error('还没轮到你')
273
+ const cards = cardIds.map(id => player.hand.find(card => card.id === id))
274
+ if (cards.length === 0 || cards.some(card => !card)) throw new Error('手牌不存在')
275
+ const combo = analyzeCards(cards)
276
+ if (!combo) throw new Error('这个牌型不合法')
277
+ if (!canBeat(combo, room.table?.combo)) throw new Error('出的牌压不过上一手')
278
+ player.hand = player.hand.filter(card => !cardIds.includes(card.id))
279
+ 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
284
+ 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}` : ''}`)
289
+ if (player.hand.length === 0) {
290
+ finishGame(room, player)
291
+ } 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)
313
+ }
314
+ room.updatedAt = Date.now()
315
+ }
316
+
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))
323
+ } else {
324
+ passTurn(room, player.id)
325
+ }
326
+ return true
327
+ }
328
+
329
+ function publicRoom(room, viewerId) {
330
+ return {
331
+ id: room.id,
332
+ ownerId: room.ownerId,
333
+ 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),
348
+ players: orderedPlayers(room).map(player => ({
349
+ id: player.id,
350
+ address: player.address,
351
+ name: player.name,
352
+ bot: player.bot,
353
+ 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) : [],
359
+ })),
360
+ }
361
+ }
362
+
363
+ export function analyzeCards(cards) {
364
+ if (!cards?.length) return null
365
+ const jokerCount = cards.filter(isJoker).length
366
+ const normals = cards.filter(card => !isJoker(card))
367
+ if (normals.length === 0) return null
368
+ const bomb = analyzeBomb(cards, normals, jokerCount)
369
+ if (bomb) return bomb
370
+ if (cards.length === 1 && jokerCount === 0) return makeCombo('single', cardValue(cards[0]), 1, [cardValue(cards[0])])
371
+ if (cards.length === 2 && canRepresentSameRank(normals, jokerCount)) {
372
+ const value = sameRankValue(normals)
373
+ return makeCombo('pair', value, 2, [value, value])
374
+ }
375
+ return analyzeStraight(cards) || analyzePairStraight(cards)
376
+ }
377
+
378
+ function analyzeBomb(cards, normals, jokerCount) {
379
+ if (cards.length < 3 || !canRepresentSameRank(normals, jokerCount)) return null
380
+ const value = sameRankValue(normals)
381
+ return makeCombo('bomb', value, cards.length, Array(cards.length).fill(value), jokerCount === 0)
382
+ }
383
+
384
+ function analyzeStraight(cards) {
385
+ if (cards.length < 3) return null
386
+ 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])) {
389
+ return makeCombo('straight', values.at(-1), cards.length, values)
390
+ }
391
+ }
392
+ return null
393
+ }
394
+
395
+ function analyzePairStraight(cards) {
396
+ if (cards.length < 4 || cards.length % 2 !== 0) return null
397
+ const pairCount = cards.length / 2
398
+ 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))
400
+ const values = pairValues.flatMap(value => [value, value])
401
+ if (cards.every((card, index) => isJoker(card) || cardValue(card) === values[index])) {
402
+ return makeCombo('pairStraight', pairValues.at(-1), cards.length, values)
403
+ }
404
+ }
405
+ return null
406
+ }
407
+
408
+ function makeCombo(type, value, length, resolvedValues, pure = true) {
409
+ return { type, value, length, resolvedValues, pure, label: labelCombo({ type, value, length, resolvedValues, pure }) }
410
+ }
411
+
412
+ function canBeat(combo, tableCombo) {
413
+ if (!combo) return false
414
+ if (!tableCombo) return true
415
+ if (combo.type === 'bomb' && tableCombo.type !== 'bomb') return true
416
+ if (combo.type !== tableCombo.type) return false
417
+ if (combo.type === 'bomb') {
418
+ if (combo.length !== tableCombo.length) return combo.length > tableCombo.length
419
+ if (combo.value !== tableCombo.value) return combo.value > tableCombo.value
420
+ return combo.pure && !tableCombo.pure
421
+ }
422
+ if (combo.length !== tableCombo.length) return false
423
+ if (combo.type === 'single' || combo.type === 'pair') {
424
+ return combo.value === nextValue(tableCombo.value) || combo.value === RANK_VALUE.get('2')
425
+ }
426
+ return combo.value === nextValue(tableCombo.value)
427
+ }
428
+
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) {
443
+ 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,
453
+ }
454
+ }
455
+
456
+ function chooseStarter(room) {
457
+ if (room.previousWinnerSeat !== null) {
458
+ room.diceRolls = []
459
+ return orderedPlayers(room).find(player => player.seat === room.previousWinnerSeat) || orderedPlayers(room)[0]
460
+ }
461
+ let candidates = orderedPlayers(room)
462
+ let rolls = []
463
+ while (candidates.length > 1) {
464
+ rolls = candidates.map(player => ({ seat: player.seat, name: player.name, value: rollDice() }))
465
+ const max = Math.max(...rolls.map(roll => roll.value))
466
+ const winners = rolls.filter(roll => roll.value === max)
467
+ if (winners.length === 1) {
468
+ room.diceRolls = rolls
469
+ return candidates.find(player => player.seat === winners[0].seat)
470
+ }
471
+ candidates = candidates.filter(player => winners.some(roll => roll.seat === player.seat))
472
+ }
473
+ room.diceRolls = rolls
474
+ return candidates[0]
475
+ }
476
+
477
+ function finishGame(room, winner) {
478
+ room.status = 'finished'
479
+ room.winnerSeat = winner.seat
480
+ room.previousWinnerSeat = winner.seat
481
+ const losers = []
482
+ let winnerGain = 0
483
+ for (const player of orderedPlayers(room)) {
484
+ if (player.seat === winner.seat) continue
485
+ const sealed = player.playedCards === 0
486
+ const loss = sealed ? SEALED_PENALTY : player.hand.length * room.baseScore
487
+ player.score -= loss
488
+ winnerGain += loss
489
+ losers.push({ seat: player.seat, name: player.name, loss, sealed, cardsLeft: player.hand.length })
490
+ }
491
+ winner.score += winnerGain
492
+ room.roundResult = { winnerSeat: winner.seat, winnerName: winner.name, winnerGain, baseScore: room.baseScore, bombCount: room.bombCount, losers }
493
+ room.log.unshift(`${winner.name} 获胜,赢 ${winnerGain} 分`)
494
+ }
495
+
496
+ function refillAfterRound(room) {
497
+ for (const player of activePlayers(room)) {
498
+ if (room.deck.length === 0) break
499
+ player.hand.push(...draw(room, 1))
500
+ sortHand(player.hand)
501
+ }
502
+ }
503
+
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) {
513
+ 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 })
517
+ }
518
+ cards.push({ id: `${deck}-SJ`, suit: 'Joker', rank: 'SJ', deck })
519
+ cards.push({ id: `${deck}-BJ`, suit: 'Joker', rank: 'BJ', deck })
520
+ }
521
+ return cards
522
+ }
523
+
524
+ function shuffle(cards) {
525
+ const copy = [...cards]
526
+ for (let index = copy.length - 1; index > 0; index -= 1) {
527
+ const swapIndex = Math.floor(Math.random() * (index + 1))
528
+ ;[copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]]
529
+ }
530
+ return copy
531
+ }
532
+
533
+ function draw(room, count) {
534
+ return room.deck.splice(0, count)
535
+ }
536
+
537
+ function sortHand(hand) {
538
+ hand.sort(compareCards)
539
+ }
540
+
541
+ function currentPlayer(room) {
542
+ return room.players.find(player => player.seat === room.currentSeat)
543
+ }
544
+
545
+ function activePlayers(room) {
546
+ return orderedPlayers(room).filter(player => player.hand.length > 0)
547
+ }
548
+
549
+ function orderedPlayers(room) {
550
+ return [...room.players].sort((a, b) => a.seat - b.seat)
551
+ }
552
+
553
+ function advanceTurn(room) {
554
+ const seats = activePlayers(room).map(player => player.seat)
555
+ const currentIndex = seats.indexOf(room.currentSeat)
556
+ room.currentSeat = seats[(currentIndex + 1) % seats.length]
557
+ }
558
+
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) {
593
+ if (normals.length === 0) return false
594
+ const first = normals[0].rank
595
+ return normals.every(card => card.rank === first) && normals.length + jokerCount >= 2
596
+ }
597
+
598
+ function sameRankValue(normals) {
599
+ return cardValue(normals[0])
600
+ }
601
+
602
+ function compareCards(a, b) {
603
+ return cardValue(a) - cardValue(b) || suitValue(a.suit) - suitValue(b.suit)
604
+ }
605
+
606
+ function cardValue(card) {
607
+ return RANK_VALUE.get(card.rank) || 99
608
+ }
609
+
610
+ function isJoker(card) {
611
+ return card.rank === 'SJ' || card.rank === 'BJ'
612
+ }
613
+
614
+ function suitValue(suit) {
615
+ return { D: 0, C: 1, H: 2, S: 3, Joker: 4 }[suit] || 0
616
+ }
617
+
618
+ function publicCard(card) {
619
+ return { id: card.id, suit: card.suit, rank: card.rank, label: labelCard(card), color: cardColor(card) }
620
+ }
621
+
622
+ function labelCard(card) {
623
+ if (card.rank === 'SJ') return '小王'
624
+ if (card.rank === 'BJ') return '大王'
625
+ return `${suitSymbol(card.suit)}${card.rank}`
626
+ }
627
+
628
+ function suitSymbol(suit) {
629
+ return { S: '♠', H: '♥', C: '♣', D: '♦' }[suit] || ''
630
+ }
631
+
632
+ function cardColor(card) {
633
+ return card.suit === 'H' || card.suit === 'D' || card.rank === 'BJ' ? 'red' : 'black'
634
+ }
635
+
636
+ function labelCombo(combo) {
637
+ const resolved = combo.resolvedValues?.length ? `(${combo.resolvedValues.map(valueLabel).join(' ')})` : ''
638
+ return `${{
639
+ single: '单张',
640
+ pair: '对子',
641
+ straight: '顺子',
642
+ pairStraight: '连对',
643
+ bomb: combo.pure ? '纯炸弹' : '带王炸弹',
644
+ }[combo.type]}${resolved}`
645
+ }
646
+
647
+ function valueLabel(value) {
648
+ return RANKS.find(rank => RANK_VALUE.get(rank) === value) || String(value)
649
+ }
650
+
651
+ function nextValue(value) {
652
+ const index = RANKS.findIndex(rank => RANK_VALUE.get(rank) === value)
653
+ if (index === -1 || index >= RANKS.length - 1) return null
654
+ return RANK_VALUE.get(RANKS[index + 1])
655
+ }
656
+
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) || '玩家'
667
+ }
668
+
669
+ function rollDice() {
670
+ return Math.floor(Math.random() * 6) + 1
671
+ }
672
+
673
+ function clamp(value, min, max) {
674
+ return Math.max(min, Math.min(max, value))
675
+ }
@@ -96,6 +96,10 @@ export function isLocalUpgradeRequest(req) {
96
96
  }
97
97
 
98
98
  export function isRemoteAccessRequest({ invite, origin, listenHost, local }) {
99
+ if (local) {
100
+ return false
101
+ }
102
+
99
103
  return (
100
104
  Boolean(invite) ||
101
105
  isExternalOrigin(origin) ||
@@ -336,6 +336,13 @@ export function createApp(engine, options = {}) {
336
336
 
337
337
  const app = new Hono()
338
338
 
339
+ app.use('/api/*', async (c, next) => {
340
+ if (c.req.header('access-control-request-private-network') === 'true') {
341
+ c.header('Access-Control-Allow-Private-Network', 'true')
342
+ }
343
+ await next()
344
+ })
345
+
339
346
  // CORS 中间件
340
347
  app.use(
341
348
  '/api/*',