most-box 0.1.2 → 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 (172) 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 +5 -5
  9. package/out/__next._full.txt +13 -13
  10. package/out/__next._head.txt +3 -3
  11. package/out/__next._index.txt +6 -6
  12. package/out/__next._tree.txt +2 -2
  13. package/out/_next/static/chunks/02zzxfop_k6tl.css +1 -0
  14. package/out/_next/static/chunks/03gsbg0fr00ey.js +1 -0
  15. package/out/_next/static/chunks/06lttvu7563zo.css +1 -0
  16. package/out/_next/static/chunks/0fo9-h4knidcz.js +1 -0
  17. package/out/_next/static/chunks/0hyds~bp.auvh.js +1 -0
  18. package/out/_next/static/chunks/0i9sfdypwuw8~.js +1 -0
  19. package/out/_next/static/chunks/0olqjomda37-e.js +1 -0
  20. package/out/_next/static/chunks/{0aq.rc9woa2nz.js → 0puk.7e.tr2zy.js} +1 -1
  21. package/out/_next/static/chunks/0s1k6rlwy02c2.js +1 -0
  22. package/out/_next/static/chunks/0sgltmtk_9s8p.css +1 -0
  23. package/out/_next/static/chunks/0snehvtvu1n4q.js +1 -0
  24. package/out/_next/static/chunks/{16xls5tt_68lx.js → 0s~g.l~x049o2.js} +1 -1
  25. package/out/_next/static/chunks/{12nr19.nnn6s3.js → 0t_3xxx4zkerp.js} +2 -2
  26. package/out/_next/static/chunks/0u38kke9vhobe.js +1 -0
  27. package/out/_next/static/chunks/0vd4_a5x-wpdh.js +1 -0
  28. package/out/_next/static/chunks/0xx_10jns1.s7.css +1 -0
  29. package/out/_next/static/chunks/{0etes81d_cihn.js → 10f-t2n4y1zx8.js} +1 -1
  30. package/out/_next/static/chunks/{0l5_.uqb-uqb8.js → 13jdyag9a-~kk.js} +1 -1
  31. package/out/_next/static/chunks/{0q0ksgxg98xgd.js → 17cwkb2yn_akx.js} +1 -1
  32. package/out/_not-found/__next._full.txt +11 -11
  33. package/out/_not-found/__next._head.txt +3 -3
  34. package/out/_not-found/__next._index.txt +6 -6
  35. package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
  36. package/out/_not-found/__next._not-found.txt +3 -3
  37. package/out/_not-found/__next._tree.txt +1 -1
  38. package/out/_not-found/index.html +2 -2
  39. package/out/_not-found/index.txt +11 -11
  40. package/out/admin/__next._full.txt +12 -12
  41. package/out/admin/__next._head.txt +3 -3
  42. package/out/admin/__next._index.txt +6 -6
  43. package/out/admin/__next._tree.txt +1 -1
  44. package/out/admin/__next.admin.__PAGE__.txt +4 -4
  45. package/out/admin/__next.admin.txt +3 -3
  46. package/out/admin/index.html +2 -2
  47. package/out/admin/index.txt +12 -12
  48. package/out/app/__next._full.txt +12 -12
  49. package/out/app/__next._head.txt +3 -3
  50. package/out/app/__next._index.txt +6 -6
  51. package/out/app/__next._tree.txt +1 -1
  52. package/out/app/__next.app.__PAGE__.txt +4 -4
  53. package/out/app/__next.app.txt +3 -3
  54. package/out/app/index.html +2 -2
  55. package/out/app/index.txt +12 -12
  56. package/out/chat/__next._full.txt +12 -12
  57. package/out/chat/__next._head.txt +3 -3
  58. package/out/chat/__next._index.txt +6 -6
  59. package/out/chat/__next._tree.txt +1 -1
  60. package/out/chat/__next.chat.__PAGE__.txt +4 -4
  61. package/out/chat/__next.chat.txt +3 -3
  62. package/out/chat/index.html +2 -2
  63. package/out/chat/index.txt +12 -12
  64. package/out/chat/join/__next._full.txt +12 -12
  65. package/out/chat/join/__next._head.txt +3 -3
  66. package/out/chat/join/__next._index.txt +6 -6
  67. package/out/chat/join/__next._tree.txt +1 -1
  68. package/out/chat/join/__next.chat.join.__PAGE__.txt +4 -4
  69. package/out/chat/join/__next.chat.join.txt +3 -3
  70. package/out/chat/join/__next.chat.txt +3 -3
  71. package/out/chat/join/index.html +2 -2
  72. package/out/chat/join/index.txt +12 -12
  73. package/out/download/__next._full.txt +34 -30
  74. package/out/download/__next._head.txt +3 -3
  75. package/out/download/__next._index.txt +6 -6
  76. package/out/download/__next._tree.txt +2 -2
  77. package/out/download/__next.download.__PAGE__.txt +8 -13
  78. package/out/download/__next.download.txt +3 -3
  79. package/out/download/index.html +2 -2
  80. package/out/download/index.txt +34 -30
  81. package/out/favicon.ico +0 -0
  82. package/out/game/__next._full.txt +20 -0
  83. package/out/game/__next._head.txt +5 -0
  84. package/out/game/__next._index.txt +9 -0
  85. package/out/game/__next._tree.txt +5 -0
  86. package/out/game/__next.game.__PAGE__.txt +6 -0
  87. package/out/game/__next.game.txt +5 -0
  88. package/out/game/gandengyan/__next._full.txt +26 -0
  89. package/out/game/gandengyan/__next._head.txt +5 -0
  90. package/out/game/gandengyan/__next._index.txt +9 -0
  91. package/out/game/gandengyan/__next._tree.txt +6 -0
  92. package/out/game/gandengyan/__next.game.gandengyan.__PAGE__.txt +10 -0
  93. package/out/game/gandengyan/__next.game.gandengyan.txt +5 -0
  94. package/out/game/gandengyan/__next.game.txt +5 -0
  95. package/out/game/gandengyan/index.html +15 -0
  96. package/out/game/gandengyan/index.txt +26 -0
  97. package/out/game/index.html +1 -0
  98. package/out/game/index.txt +20 -0
  99. package/out/game/zhajinhua/__next._full.txt +25 -0
  100. package/out/game/zhajinhua/__next._head.txt +5 -0
  101. package/out/game/zhajinhua/__next._index.txt +9 -0
  102. package/out/game/zhajinhua/__next._tree.txt +5 -0
  103. package/out/game/zhajinhua/__next.game.txt +5 -0
  104. package/out/game/zhajinhua/__next.game.zhajinhua.__PAGE__.txt +9 -0
  105. package/out/game/zhajinhua/__next.game.zhajinhua.txt +5 -0
  106. package/out/game/zhajinhua/index.html +15 -0
  107. package/out/game/zhajinhua/index.txt +25 -0
  108. package/out/index.html +2 -2
  109. package/out/index.txt +13 -13
  110. package/out/note/__next._full.txt +12 -12
  111. package/out/note/__next._head.txt +3 -3
  112. package/out/note/__next._index.txt +6 -6
  113. package/out/note/__next._tree.txt +1 -1
  114. package/out/note/__next.note.__PAGE__.txt +4 -4
  115. package/out/note/__next.note.txt +3 -3
  116. package/out/note/index.html +2 -2
  117. package/out/note/index.txt +12 -12
  118. package/out/ping/__next._full.txt +12 -12
  119. package/out/ping/__next._head.txt +3 -3
  120. package/out/ping/__next._index.txt +6 -6
  121. package/out/ping/__next._tree.txt +1 -1
  122. package/out/ping/__next.ping.__PAGE__.txt +4 -4
  123. package/out/ping/__next.ping.txt +3 -3
  124. package/out/ping/index.html +2 -2
  125. package/out/ping/index.txt +12 -12
  126. package/out/web3/__next._full.txt +12 -12
  127. package/out/web3/__next._head.txt +3 -3
  128. package/out/web3/__next._index.txt +6 -6
  129. package/out/web3/__next._tree.txt +1 -1
  130. package/out/web3/__next.web3.__PAGE__.txt +4 -4
  131. package/out/web3/__next.web3.txt +3 -3
  132. package/out/web3/ed25519/__next._full.txt +10 -10
  133. package/out/web3/ed25519/__next._head.txt +3 -3
  134. package/out/web3/ed25519/__next._index.txt +6 -6
  135. package/out/web3/ed25519/__next._tree.txt +1 -1
  136. package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
  137. package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
  138. package/out/web3/ed25519/__next.web3.txt +3 -3
  139. package/out/web3/ed25519/index.html +1 -1
  140. package/out/web3/ed25519/index.txt +10 -10
  141. package/out/web3/index.html +2 -2
  142. package/out/web3/index.txt +12 -12
  143. package/out/web3/tools/__next._full.txt +10 -10
  144. package/out/web3/tools/__next._head.txt +3 -3
  145. package/out/web3/tools/__next._index.txt +6 -6
  146. package/out/web3/tools/__next._tree.txt +1 -1
  147. package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
  148. package/out/web3/tools/__next.web3.tools.txt +3 -3
  149. package/out/web3/tools/__next.web3.txt +3 -3
  150. package/out/web3/tools/index.html +1 -1
  151. package/out/web3/tools/index.txt +10 -10
  152. package/package.json +29 -29
  153. package/public/favicon.ico +0 -0
  154. package/server/src/config.js +1 -1
  155. package/server/src/core/gameRoom.js +222 -0
  156. package/server/src/core/zhajinhua.js +563 -0
  157. package/server/src/games/gandengyan.js +612 -0
  158. package/server/src/http/access.js +4 -0
  159. package/server/src/http/app.js +14 -1
  160. package/server/src/index.js +9 -1
  161. package/server/src/utils/api.js +19 -1
  162. package/out/_next/static/chunks/0.e2avjgna_b2.js +0 -1
  163. package/out/_next/static/chunks/03h~nhgj0hv3p.css +0 -1
  164. package/out/_next/static/chunks/07td.jq7xff84.css +0 -1
  165. package/out/_next/static/chunks/0gwian.hp3-92.js +0 -1
  166. package/out/_next/static/chunks/0mex8svsiv-2l.js +0 -1
  167. package/out/_next/static/chunks/0myq9gs8szydh.js +0 -1
  168. package/out/_next/static/chunks/0p0sv~fuddvgr.js +0 -1
  169. package/out/_next/static/chunks/0wtf0xsiicxx6.js +0 -1
  170. /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → sSvbBrwXZY-4lBmcHshga}/_buildManifest.js +0 -0
  171. /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → sSvbBrwXZY-4lBmcHshga}/_clientMiddlewareManifest.js +0 -0
  172. /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → sSvbBrwXZY-4lBmcHshga}/_ssgManifest.js +0 -0
@@ -0,0 +1,612 @@
1
+ const SUITS = ['S', 'H', 'C', 'D']
2
+ const RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2']
3
+ const STRAIGHT_RANKS = RANKS.filter(rank => rank !== '2')
4
+ const RANK_VALUE = new Map(RANKS.map((rank, index) => [rank, index + 3]))
5
+ const INITIAL_HAND_SIZE = 5
6
+ const SEALED_PENALTY = 15
7
+
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
+ })
21
+ }
22
+
23
+ const room = {
24
+ id: String(roomCode || '').toUpperCase(),
25
+ ownerAddress: normalizeAddress(ownerAddress),
26
+ status: 'lobby',
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
+ })),
37
+ deck: [],
38
+ discard: [],
39
+ table: null,
40
+ currentSeat: 0,
41
+ lastWinnerSeat: null,
42
+ previousWinnerSeat: null,
43
+ passSeats: [],
44
+ baseScore: 1,
45
+ bombCount: 0,
46
+ diceRolls: [],
47
+ roundResult: null,
48
+ log: ['房间已创建'],
49
+ winnerSeat: null,
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
92
+ player.playedCards = 0
93
+ sortHand(player.hand)
94
+ }
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
+ )
105
+ }
106
+ state.seq += 1
107
+ return publicGanDengYanRoom(state)
108
+ }
109
+
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
+
118
+ const cards = cardIds.map(id => player.hand.find(card => card.id === id))
119
+ if (cards.length === 0 || cards.some(card => !card)) {
120
+ return { ok: false, error: '手牌不存在', state: publicGanDengYanRoom(state) }
121
+ }
122
+ const combo = analyzeCards(cards)
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
+
130
+ player.hand = player.hand.filter(card => !cardIds.includes(card.id))
131
+ player.handCount = player.hand.length
132
+ player.playedCards += cards.length
133
+ state.discard.push(...cards)
134
+ state.table = { seat: player.seat, playerName: player.name, cards, combo }
135
+ state.passSeats = []
136
+ state.lastWinnerSeat = player.seat
137
+ if (combo.type === 'bomb') {
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
+ )
146
+ if (player.hand.length === 0) {
147
+ finishGame(state, player)
148
+ } else {
149
+ advanceTurn(state)
150
+ }
151
+ state.seq += 1
152
+ return { ok: true, state: publicGanDengYanRoom(state) }
153
+ }
154
+
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 张,重新领出')
176
+ } else {
177
+ advanceTurn(state)
178
+ }
179
+ state.seq += 1
180
+ return { ok: true, state: publicGanDengYanRoom(state) }
181
+ }
182
+
183
+ export function publicGanDengYanRoom(room) {
184
+ if (!room) return null
185
+ return {
186
+ id: room.id,
187
+ ownerAddress: room.ownerAddress,
188
+ status: room.status,
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) : [],
214
+ players: orderedPlayers(room).map(player => ({
215
+ address: player.address,
216
+ name: player.name,
217
+ seat: player.seat,
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) : [],
222
+ })),
223
+ }
224
+ }
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
+
274
+ export function analyzeCards(cards) {
275
+ if (!cards?.length) return null
276
+ const jokerCount = cards.filter(isJoker).length
277
+ const normals = cards.filter(card => !isJoker(card))
278
+ if (normals.length === 0) return null
279
+ const bomb = analyzeBomb(cards, normals, jokerCount)
280
+ if (bomb) return bomb
281
+ if (cards.length === 1 && jokerCount === 0) {
282
+ return makeCombo('single', cardValue(cards[0]), 1, [cardValue(cards[0])])
283
+ }
284
+ if (cards.length === 2 && canRepresentSameRank(normals, jokerCount)) {
285
+ const value = sameRankValue(normals)
286
+ return makeCombo('pair', value, 2, [value, value])
287
+ }
288
+ return analyzeStraight(cards) || analyzePairStraight(cards)
289
+ }
290
+
291
+ function analyzeBomb(cards, normals, jokerCount) {
292
+ if (cards.length < 3 || !canRepresentSameRank(normals, jokerCount)) return null
293
+ const value = sameRankValue(normals)
294
+ return makeCombo(
295
+ 'bomb',
296
+ value,
297
+ cards.length,
298
+ Array(cards.length).fill(value),
299
+ jokerCount === 0
300
+ )
301
+ }
302
+
303
+ function analyzeStraight(cards) {
304
+ if (cards.length < 3) return null
305
+ for (let start = 0; start <= STRAIGHT_RANKS.length - cards.length; start += 1) {
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
+ ) {
314
+ return makeCombo('straight', values.at(-1), cards.length, values)
315
+ }
316
+ }
317
+ return null
318
+ }
319
+
320
+ function analyzePairStraight(cards) {
321
+ if (cards.length < 4 || cards.length % 2 !== 0) return null
322
+ const pairCount = cards.length / 2
323
+ for (let start = 0; start <= STRAIGHT_RANKS.length - pairCount; start += 1) {
324
+ const pairValues = STRAIGHT_RANKS.slice(start, start + pairCount).map(rank =>
325
+ RANK_VALUE.get(rank)
326
+ )
327
+ const values = pairValues.flatMap(value => [value, value])
328
+ if (
329
+ cards.every(
330
+ (card, index) => isJoker(card) || cardValue(card) === values[index]
331
+ )
332
+ ) {
333
+ return makeCombo('pairStraight', pairValues.at(-1), cards.length, values)
334
+ }
335
+ }
336
+ return null
337
+ }
338
+
339
+ function makeCombo(type, value, length, resolvedValues, pure = true) {
340
+ return {
341
+ type,
342
+ value,
343
+ length,
344
+ resolvedValues,
345
+ pure,
346
+ label: labelCombo({ type, value, length, resolvedValues, pure }),
347
+ }
348
+ }
349
+
350
+ function canBeat(combo, tableCombo) {
351
+ if (!combo) return false
352
+ if (!tableCombo) return true
353
+ if (combo.type === 'bomb' && tableCombo.type !== 'bomb') return true
354
+ if (combo.type !== tableCombo.type) return false
355
+ if (combo.type === 'bomb') {
356
+ if (combo.length !== tableCombo.length) return combo.length > tableCombo.length
357
+ if (combo.value !== tableCombo.value) return combo.value > tableCombo.value
358
+ return combo.pure && !tableCombo.pure
359
+ }
360
+ if (combo.length !== tableCombo.length) return false
361
+ if (combo.type === 'single' || combo.type === 'pair') {
362
+ return combo.value === nextValue(tableCombo.value) || combo.value === RANK_VALUE.get('2')
363
+ }
364
+ return combo.value === nextValue(tableCombo.value)
365
+ }
366
+
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
402
+ return {
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 }),
408
+ }
409
+ }
410
+
411
+ function chooseStarter(room, random) {
412
+ if (room.previousWinnerSeat !== null) {
413
+ room.diceRolls = []
414
+ return orderedPlayers(room).find(player => player.seat === room.previousWinnerSeat) || orderedPlayers(room)[0]
415
+ }
416
+ let candidates = orderedPlayers(room)
417
+ let rolls = []
418
+ while (candidates.length > 1) {
419
+ rolls = candidates.map(player => ({
420
+ seat: player.seat,
421
+ name: player.name,
422
+ value: rollDice(random),
423
+ }))
424
+ const max = Math.max(...rolls.map(roll => roll.value))
425
+ const winners = rolls.filter(roll => roll.value === max)
426
+ if (winners.length === 1) {
427
+ room.diceRolls = rolls
428
+ return candidates.find(player => player.seat === winners[0].seat)
429
+ }
430
+ candidates = candidates.filter(player =>
431
+ winners.some(roll => roll.seat === player.seat)
432
+ )
433
+ }
434
+ room.diceRolls = rolls
435
+ return candidates[0]
436
+ }
437
+
438
+ function finishGame(room, winner) {
439
+ room.status = 'finished'
440
+ room.winnerSeat = winner.seat
441
+ room.previousWinnerSeat = winner.seat
442
+ const losers = []
443
+ let winnerGain = 0
444
+ for (const player of orderedPlayers(room)) {
445
+ if (player.seat === winner.seat) continue
446
+ const sealed = player.playedCards === 0
447
+ const loss = sealed ? SEALED_PENALTY : player.hand.length * room.baseScore
448
+ player.score -= loss
449
+ winnerGain += loss
450
+ losers.push({
451
+ seat: player.seat,
452
+ name: player.name,
453
+ loss,
454
+ sealed,
455
+ cardsLeft: player.hand.length,
456
+ })
457
+ }
458
+ winner.score += winnerGain
459
+ room.roundResult = {
460
+ winnerSeat: winner.seat,
461
+ winnerName: winner.name,
462
+ winnerGain,
463
+ baseScore: room.baseScore,
464
+ bombCount: room.bombCount,
465
+ losers,
466
+ }
467
+ room.log.unshift(`${winner.name} 获胜,赢 ${winnerGain} 分`)
468
+ }
469
+
470
+ function refillAfterRound(room) {
471
+ for (const player of activePlayers(room)) {
472
+ if (room.deck.length === 0) break
473
+ player.hand.push(...draw(room, 1))
474
+ player.handCount = player.hand.length
475
+ sortHand(player.hand)
476
+ }
477
+ }
478
+
479
+ function createDeck() {
480
+ const cards = []
481
+ for (const suit of SUITS) {
482
+ for (const rank of RANKS) {
483
+ cards.push({ id: `${suit}-${rank}`, suit, rank })
484
+ }
485
+ }
486
+ cards.push({ id: 'SJ', suit: 'Joker', rank: 'SJ' })
487
+ cards.push({ id: 'BJ', suit: 'Joker', rank: 'BJ' })
488
+ return cards.map(publicCard)
489
+ }
490
+
491
+ function shuffle(cards, random) {
492
+ const copy = [...cards]
493
+ for (let index = copy.length - 1; index > 0; index -= 1) {
494
+ const swapIndex = Math.floor(random() * (index + 1))
495
+ ;[copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]]
496
+ }
497
+ return copy
498
+ }
499
+
500
+ function draw(room, count) {
501
+ return room.deck.splice(0, count)
502
+ }
503
+
504
+ function sortHand(hand) {
505
+ hand.sort(compareCards)
506
+ }
507
+
508
+ function currentPlayer(room) {
509
+ return room.players.find(player => player.seat === room.currentSeat)
510
+ }
511
+
512
+ function activePlayers(room) {
513
+ return orderedPlayers(room).filter(player => player.hand.length > 0)
514
+ }
515
+
516
+ function orderedPlayers(room) {
517
+ return [...(room?.players || [])].sort((a, b) => a.seat - b.seat)
518
+ }
519
+
520
+ function advanceTurn(room) {
521
+ const seats = activePlayers(room).map(player => player.seat)
522
+ const currentIndex = seats.indexOf(room.currentSeat)
523
+ room.currentSeat = seats[(currentIndex + 1) % seats.length]
524
+ }
525
+
526
+ function canRepresentSameRank(normals, jokerCount = 0) {
527
+ if (normals.length === 0) return false
528
+ const first = normals[0].rank
529
+ return normals.every(card => card.rank === first) && normals.length + jokerCount >= 2
530
+ }
531
+
532
+ function sameRankValue(normals) {
533
+ return cardValue(normals[0])
534
+ }
535
+
536
+ function compareCards(a, b) {
537
+ return cardValue(a) - cardValue(b) || suitValue(a.suit) - suitValue(b.suit)
538
+ }
539
+
540
+ function cardValue(card) {
541
+ return RANK_VALUE.get(card.rank) || 99
542
+ }
543
+
544
+ function isJoker(card) {
545
+ return card.rank === 'SJ' || card.rank === 'BJ'
546
+ }
547
+
548
+ function suitValue(suit) {
549
+ return { D: 0, C: 1, H: 2, S: 3, Joker: 4 }[suit] || 0
550
+ }
551
+
552
+ function publicCard(card) {
553
+ return {
554
+ id: card.id,
555
+ suit: card.suit,
556
+ rank: card.rank,
557
+ label: labelCard(card),
558
+ color: cardColor(card),
559
+ }
560
+ }
561
+
562
+ function labelCard(card) {
563
+ if (card.rank === 'SJ') return '小王'
564
+ if (card.rank === 'BJ') return '大王'
565
+ return `${suitSymbol(card.suit)}${card.rank}`
566
+ }
567
+
568
+ function suitSymbol(suit) {
569
+ return { S: '♠', H: '♥', C: '♣', D: '♦' }[suit] || ''
570
+ }
571
+
572
+ function cardColor(card) {
573
+ return card.suit === 'H' || card.suit === 'D' || card.rank === 'BJ'
574
+ ? 'red'
575
+ : 'black'
576
+ }
577
+
578
+ function labelCombo(combo) {
579
+ const resolved = combo.resolvedValues?.length
580
+ ? `(${combo.resolvedValues.map(valueLabel).join(' ')})`
581
+ : ''
582
+ return `${{
583
+ single: '单张',
584
+ pair: '对子',
585
+ straight: '顺子',
586
+ pairStraight: '连对',
587
+ bomb: combo.pure ? '纯炸弹' : '带王炸弹',
588
+ }[combo.type]}${resolved}`
589
+ }
590
+
591
+ function valueLabel(value) {
592
+ return RANKS.find(rank => RANK_VALUE.get(rank) === value) || String(value)
593
+ }
594
+
595
+ function nextValue(value) {
596
+ const index = RANKS.findIndex(rank => RANK_VALUE.get(rank) === value)
597
+ if (index === -1 || index >= RANKS.length - 1) return null
598
+ return RANK_VALUE.get(RANKS[index + 1])
599
+ }
600
+
601
+ function rollDice(random) {
602
+ return Math.floor(random() * 6) + 1
603
+ }
604
+
605
+ function normalizeAddress(value) {
606
+ const address = String(value || '').trim()
607
+ return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''
608
+ }
609
+
610
+ function cleanName(name) {
611
+ return String(name || '玩家').trim().slice(0, 16) || '玩家'
612
+ }
@@ -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/*',
@@ -952,7 +959,13 @@ export function createApp(engine, options = {}) {
952
959
  })
953
960
 
954
961
  app.get('/api/channels', c => {
955
- return c.json(engine.listChannels({ ownerAddress: c.get('userAddress') }))
962
+ return c.json(
963
+ engine.listChannels({
964
+ ownerAddress: c.get('userAddress'),
965
+ type: c.req.query('type'),
966
+ excludeType: c.req.query('excludeType'),
967
+ })
968
+ )
956
969
  })
957
970
 
958
971
  app.delete('/api/channels/:name', async c => {
@@ -1834,6 +1834,7 @@ export class MostBoxEngine extends EventEmitter {
1834
1834
  async createChannel(name, type = 'personal', options = {}) {
1835
1835
  this.#ensureInitialized()
1836
1836
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
1837
+ const channelType = String(type || 'personal').trim() || 'personal'
1837
1838
 
1838
1839
  if (!CHANNEL_NAME_REGEX.test(name)) {
1839
1840
  throw new Error('频道名只能包含字母、数字、下划线和连字符')
@@ -1879,7 +1880,7 @@ export class MostBoxEngine extends EventEmitter {
1879
1880
  discoveryKey: b4a.toString(discoveryKey, 'hex'),
1880
1881
  coreKey: b4a.toString(core.key, 'hex'),
1881
1882
  createdAt: new Date().toISOString(),
1882
- type,
1883
+ type: channelType,
1883
1884
  ownerAddress,
1884
1885
  members: ownerAddress ? [ownerAddress] : [],
1885
1886
  remoteCoreKeys: [],
@@ -2105,12 +2106,19 @@ export class MostBoxEngine extends EventEmitter {
2105
2106
  listChannels(options = {}) {
2106
2107
  this.#ensureInitialized()
2107
2108
  const ownerAddress = normalizeOwnerAddress(options.ownerAddress)
2109
+ const type = String(options.type || '').trim()
2110
+ const excludeType = String(options.excludeType || '').trim()
2108
2111
 
2109
2112
  return this.#channels
2110
2113
  .filter(c => {
2111
2114
  if (!ownerAddress) return true
2112
2115
  return Array.isArray(c.members) && c.members.includes(ownerAddress)
2113
2116
  })
2117
+ .filter(c => {
2118
+ if (type) return c.type === type
2119
+ if (excludeType) return c.type !== excludeType
2120
+ return true
2121
+ })
2114
2122
  .map(c => ({
2115
2123
  name: c.name,
2116
2124
  coreKey: c.coreKey,