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,563 @@
1
+ export const ZHJ_INITIAL_CHIPS = 1000
2
+ export const ZHJ_ANTE = 10
3
+ export const ZHJ_MIN_PLAYERS = 2
4
+ export const ZHJ_MAX_PLAYERS = 5
5
+ export const ZHJ_RAISE_STEPS = [10, 20, 50, 100]
6
+
7
+ const RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
8
+ const SUITS = ['D', 'C', 'H', 'S']
9
+ const RANK_VALUE = new Map(RANKS.map((rank, index) => [rank, index + 2]))
10
+ const SUIT_VALUE = new Map(SUITS.map((suit, index) => [suit, index + 1]))
11
+
12
+ const CATEGORY = {
13
+ high: 1,
14
+ pair: 2,
15
+ straight: 3,
16
+ flush: 4,
17
+ straightFlush: 5,
18
+ triple: 6,
19
+ }
20
+
21
+ const CATEGORY_LABEL = {
22
+ [CATEGORY.high]: '散牌',
23
+ [CATEGORY.pair]: '对子',
24
+ [CATEGORY.straight]: '顺子',
25
+ [CATEGORY.flush]: '金花',
26
+ [CATEGORY.straightFlush]: '顺金',
27
+ [CATEGORY.triple]: '豹子',
28
+ }
29
+
30
+ export function normalizeRoomCode(input) {
31
+ return String(input || '')
32
+ .trim()
33
+ .toUpperCase()
34
+ .replace(/[^A-Z0-9]/g, '')
35
+ .slice(0, 8)
36
+ }
37
+
38
+ export function createEventId(prefix = 'zhajinhua') {
39
+ return `${prefix}_${Date.now()}_${randomHex(4)}`
40
+ }
41
+
42
+ export function createRoundId() {
43
+ return createEventId('round')
44
+ }
45
+
46
+ export function createDeck() {
47
+ const deck = []
48
+ for (const suit of SUITS) {
49
+ for (const rank of RANKS) {
50
+ deck.push(`${rank}${suit}`)
51
+ }
52
+ }
53
+ return deck
54
+ }
55
+
56
+ export function shuffleDeck(inputDeck = createDeck(), random = Math.random) {
57
+ const deck = [...inputDeck]
58
+ for (let i = deck.length - 1; i > 0; i--) {
59
+ const j = Math.floor(random() * (i + 1))
60
+ ;[deck[i], deck[j]] = [deck[j], deck[i]]
61
+ }
62
+ return deck
63
+ }
64
+
65
+ export function parseCard(card) {
66
+ const value = String(card || '').trim().toUpperCase()
67
+ const suit = value.slice(-1)
68
+ const rank = value.slice(0, -1)
69
+ if (!RANK_VALUE.has(rank) || !SUIT_VALUE.has(suit)) {
70
+ throw new Error(`Invalid card: ${card}`)
71
+ }
72
+ return {
73
+ id: `${rank}${suit}`,
74
+ rank,
75
+ suit,
76
+ rankValue: RANK_VALUE.get(rank),
77
+ suitValue: SUIT_VALUE.get(suit),
78
+ }
79
+ }
80
+
81
+ function compareNumberArrays(a, b) {
82
+ const length = Math.max(a.length, b.length)
83
+ for (let i = 0; i < length; i++) {
84
+ const left = a[i] || 0
85
+ const right = b[i] || 0
86
+ if (left !== right) return left - right
87
+ }
88
+ return 0
89
+ }
90
+
91
+ function getStraightHigh(rankValues) {
92
+ const values = [...new Set(rankValues)].sort((a, b) => a - b)
93
+ if (values.length !== 3) return 0
94
+ if (values[0] === 2 && values[1] === 3 && values[2] === 14) return 3
95
+ if (values[0] + 1 === values[1] && values[1] + 1 === values[2]) {
96
+ return values[2]
97
+ }
98
+ return 0
99
+ }
100
+
101
+ export function evaluateHand(cards) {
102
+ if (!Array.isArray(cards) || cards.length !== 3) {
103
+ throw new Error('炸金花手牌必须是 3 张牌')
104
+ }
105
+
106
+ const parsed = cards.map(parseCard)
107
+ const ranks = parsed.map(card => card.rankValue)
108
+ const rankCounts = new Map()
109
+ for (const rank of ranks) {
110
+ rankCounts.set(rank, (rankCounts.get(rank) || 0) + 1)
111
+ }
112
+
113
+ const isFlush = new Set(parsed.map(card => card.suit)).size === 1
114
+ const straightHigh = getStraightHigh(ranks)
115
+ const sortedRanks = [...ranks].sort((a, b) => b - a)
116
+ const tieSuit = Math.max(...parsed.map(card => card.suitValue))
117
+
118
+ let category = CATEGORY.high
119
+ let tiebreakers = sortedRanks
120
+
121
+ if (rankCounts.size === 1) {
122
+ category = CATEGORY.triple
123
+ tiebreakers = [sortedRanks[0]]
124
+ } else if (straightHigh && isFlush) {
125
+ category = CATEGORY.straightFlush
126
+ tiebreakers = [straightHigh]
127
+ } else if (isFlush) {
128
+ category = CATEGORY.flush
129
+ tiebreakers = sortedRanks
130
+ } else if (straightHigh) {
131
+ category = CATEGORY.straight
132
+ tiebreakers = [straightHigh]
133
+ } else if (rankCounts.size === 2) {
134
+ category = CATEGORY.pair
135
+ const pairRank = [...rankCounts.entries()].find(([, count]) => count === 2)[0]
136
+ const kicker = [...rankCounts.entries()].find(([, count]) => count === 1)[0]
137
+ tiebreakers = [pairRank, kicker]
138
+ }
139
+
140
+ return {
141
+ category,
142
+ label: CATEGORY_LABEL[category],
143
+ tiebreakers,
144
+ tieSuit,
145
+ cards: parsed.map(card => card.id),
146
+ }
147
+ }
148
+
149
+ export function compareHands(leftCards, rightCards) {
150
+ const left = evaluateHand(leftCards)
151
+ const right = evaluateHand(rightCards)
152
+ if (left.category !== right.category) return left.category - right.category
153
+ const rankDiff = compareNumberArrays(left.tiebreakers, right.tiebreakers)
154
+ if (rankDiff !== 0) return rankDiff
155
+ return left.tieSuit - right.tieSuit
156
+ }
157
+
158
+ export function getHandLabel(cards) {
159
+ return evaluateHand(cards).label
160
+ }
161
+
162
+ export function createPlayerActionEvent({
163
+ roundId,
164
+ action,
165
+ amount,
166
+ target,
167
+ }) {
168
+ return {
169
+ roundId,
170
+ eventId: createEventId('action'),
171
+ action,
172
+ amount,
173
+ target,
174
+ }
175
+ }
176
+
177
+ export function canStartRound(players = []) {
178
+ const eligible = players.filter(player => Number(player.chips) >= ZHJ_ANTE)
179
+ return eligible.length >= ZHJ_MIN_PLAYERS && eligible.length <= ZHJ_MAX_PLAYERS
180
+ }
181
+
182
+ export function startRound({ roomCode, players, hostAddress, roundId = createRoundId(), random = Math.random }) {
183
+ const participants = players
184
+ .filter(player => Number(player.chips) >= ZHJ_ANTE)
185
+ .slice(0, ZHJ_MAX_PLAYERS)
186
+
187
+ if (participants.length < ZHJ_MIN_PLAYERS) {
188
+ throw new Error('至少需要 2 名有足够筹码的玩家')
189
+ }
190
+
191
+ const deck = shuffleDeck(createDeck(), random)
192
+ const hands = {}
193
+ const publicPlayers = participants.map((player, index) => {
194
+ const cards = deck.slice(index * 3, index * 3 + 3)
195
+ hands[player.address] = cards
196
+ return {
197
+ address: player.address,
198
+ name: player.name,
199
+ publicKey: player.publicKey,
200
+ chips: Number(player.chips) - ZHJ_ANTE,
201
+ status: 'active',
202
+ looked: false,
203
+ bet: ZHJ_ANTE,
204
+ }
205
+ })
206
+
207
+ const hostIndex = Math.max(
208
+ 0,
209
+ publicPlayers.findIndex(player => player.address === normalizeAddress(hostAddress))
210
+ )
211
+ const turnAddress = publicPlayers[(hostIndex + 1) % publicPlayers.length].address
212
+ const round = {
213
+ roomCode: normalizeRoomCode(roomCode),
214
+ roundId,
215
+ status: 'playing',
216
+ host: normalizeAddress(hostAddress),
217
+ seq: 1,
218
+ startedAt: Date.now(),
219
+ ante: ZHJ_ANTE,
220
+ pot: ZHJ_ANTE * publicPlayers.length,
221
+ currentBet: ZHJ_ANTE,
222
+ turnAddress,
223
+ players: publicPlayers,
224
+ lastAction: '本局开始',
225
+ winner: null,
226
+ showdown: null,
227
+ appliedEventIds: [],
228
+ hands,
229
+ }
230
+
231
+ return round
232
+ }
233
+
234
+ export function getPublicRoundState(round) {
235
+ if (!round) return null
236
+ const publicState = { ...round }
237
+ delete publicState.hands
238
+ return clone(publicState)
239
+ }
240
+
241
+ export function hydrateRoundWithHands(publicRound, hands) {
242
+ const round = normalizePublicRoundState(publicRound)
243
+ if (!round || !hands || typeof hands !== 'object') return null
244
+ return {
245
+ ...round,
246
+ hands: clone(hands),
247
+ }
248
+ }
249
+
250
+ export function normalizePublicRoundState(input) {
251
+ if (!input || typeof input !== 'object') return null
252
+ if (!input.roundId || !Array.isArray(input.players)) return null
253
+ return {
254
+ roomCode: normalizeRoomCode(input.roomCode),
255
+ roundId: String(input.roundId),
256
+ status: input.status === 'finished' ? 'finished' : 'playing',
257
+ host: normalizeAddress(input.host),
258
+ seq: Number(input.seq) || 1,
259
+ startedAt: Number(input.startedAt) || Date.now(),
260
+ ante: Number(input.ante) || ZHJ_ANTE,
261
+ pot: Math.max(0, Number(input.pot) || 0),
262
+ currentBet: Math.max(0, Number(input.currentBet) || ZHJ_ANTE),
263
+ turnAddress: normalizeAddress(input.turnAddress),
264
+ players: input.players.map(normalizeRoundPlayer).filter(Boolean),
265
+ lastAction: String(input.lastAction || ''),
266
+ winner: input.winner ? normalizeAddress(input.winner) : null,
267
+ showdown: input.showdown && typeof input.showdown === 'object' ? clone(input.showdown) : null,
268
+ appliedEventIds: Array.isArray(input.appliedEventIds)
269
+ ? input.appliedEventIds.map(String)
270
+ : [],
271
+ }
272
+ }
273
+
274
+ export function validatePlayerAction(round, actionEvent, authorAddress) {
275
+ const state = normalizePublicRoundState(round)
276
+ if (!state || state.status !== 'playing') return { ok: false, error: '当前没有进行中的牌局' }
277
+
278
+ const author = normalizeAddress(authorAddress)
279
+ const player = state.players.find(item => item.address === author)
280
+ if (!player || player.status !== 'active') return { ok: false, error: '玩家不在本轮牌局中' }
281
+ if (state.turnAddress !== author) return { ok: false, error: '还没有轮到你操作' }
282
+
283
+ const action = actionEvent?.action
284
+ if (!['look', 'call', 'raise', 'compare', 'fold'].includes(action)) {
285
+ return { ok: false, error: '未知操作' }
286
+ }
287
+
288
+ if (action === 'look') {
289
+ return player.looked ? { ok: false, error: '已经看过牌' } : { ok: true }
290
+ }
291
+
292
+ if (action === 'call') {
293
+ const need = Math.max(0, state.currentBet - player.bet)
294
+ return player.chips >= need ? { ok: true } : { ok: false, error: '筹码不足,不能跟注' }
295
+ }
296
+
297
+ if (action === 'raise') {
298
+ const amount = Number(actionEvent.amount)
299
+ if (!ZHJ_RAISE_STEPS.includes(amount)) {
300
+ return { ok: false, error: '加注档位无效' }
301
+ }
302
+ const need = state.currentBet + amount - player.bet
303
+ return player.chips >= need ? { ok: true } : { ok: false, error: '筹码不足,不能加注' }
304
+ }
305
+
306
+ if (action === 'compare') {
307
+ const target = normalizeAddress(actionEvent.target)
308
+ const targetPlayer = state.players.find(item => item.address === target)
309
+ if (!target || target === author || !targetPlayer || targetPlayer.status !== 'active') {
310
+ return { ok: false, error: '请选择有效的比牌对象' }
311
+ }
312
+ const need = Math.max(0, state.currentBet - player.bet)
313
+ return player.chips >= need ? { ok: true } : { ok: false, error: '筹码不足,不能比牌' }
314
+ }
315
+
316
+ return { ok: true }
317
+ }
318
+
319
+ export function applyPlayerAction(round, actionEvent, authorAddress) {
320
+ const validation = validatePlayerAction(round, actionEvent, authorAddress)
321
+ if (!validation.ok) return { ok: false, error: validation.error, state: round }
322
+
323
+ const state = clone(round)
324
+ const author = normalizeAddress(authorAddress)
325
+ const player = state.players.find(item => item.address === author)
326
+ const eventId = String(actionEvent.eventId)
327
+
328
+ if (state.appliedEventIds?.includes(eventId)) {
329
+ return { ok: true, state, duplicate: true }
330
+ }
331
+
332
+ if (!Array.isArray(state.appliedEventIds)) state.appliedEventIds = []
333
+ state.appliedEventIds.push(eventId)
334
+
335
+ if (actionEvent.action === 'look') {
336
+ player.looked = true
337
+ state.lastAction = `${player.name} 看牌`
338
+ return advanceSeq(state)
339
+ }
340
+
341
+ if (actionEvent.action === 'fold') {
342
+ player.status = 'folded'
343
+ state.lastAction = `${player.name} 弃牌`
344
+ return maybeFinishOrAdvance(state, author)
345
+ }
346
+
347
+ if (actionEvent.action === 'call') {
348
+ payToCurrentBet(state, player)
349
+ state.lastAction = `${player.name} 跟注`
350
+ return maybeFinishOrAdvance(state, author)
351
+ }
352
+
353
+ if (actionEvent.action === 'raise') {
354
+ const amount = Number(actionEvent.amount)
355
+ state.currentBet += amount
356
+ payToCurrentBet(state, player)
357
+ state.lastAction = `${player.name} 加注 ${amount}`
358
+ return maybeFinishOrAdvance(state, author)
359
+ }
360
+
361
+ if (actionEvent.action === 'compare') {
362
+ payToCurrentBet(state, player)
363
+ const targetAddress = normalizeAddress(actionEvent.target)
364
+ const targetPlayer = state.players.find(item => item.address === targetAddress)
365
+ const diff = compareHands(state.hands?.[author] || [], state.hands?.[targetAddress] || [])
366
+ const loser = diff >= 0 ? targetPlayer : player
367
+ loser.status = 'folded'
368
+ state.lastAction = `${player.name} 与 ${targetPlayer.name} 比牌,${loser.name} 出局`
369
+ return maybeFinishOrAdvance(state, author)
370
+ }
371
+
372
+ return { ok: false, error: '未知操作', state: round }
373
+ }
374
+
375
+ export function getActiveRoundPlayers(round) {
376
+ return (round?.players || []).filter(player => player.status === 'active')
377
+ }
378
+
379
+ export function getAllowedActions(round, address) {
380
+ const player = (round?.players || []).find(item => item.address === normalizeAddress(address))
381
+ if (!round || round.status !== 'playing' || !player || player.status !== 'active') return []
382
+ if (round.turnAddress !== player.address) return []
383
+
384
+ const actions = []
385
+ if (!player.looked) actions.push('look')
386
+ if (validatePlayerAction(round, { action: 'call' }, player.address).ok) actions.push('call')
387
+ if (ZHJ_RAISE_STEPS.some(amount => validatePlayerAction(round, { action: 'raise', amount }, player.address).ok)) {
388
+ actions.push('raise')
389
+ }
390
+ if (getActiveRoundPlayers(round).length > 1) actions.push('compare')
391
+ actions.push('fold')
392
+ return actions
393
+ }
394
+
395
+ export function chooseBotAction(round, address, random = Math.random) {
396
+ const botAddress = normalizeAddress(address)
397
+ const allowedActions = getAllowedActions(round, botAddress)
398
+ if (allowedActions.length === 0) return null
399
+
400
+ const player = (round?.players || []).find(item => item.address === botAddress)
401
+ const handStrength = estimateHandStrength(round?.hands?.[botAddress])
402
+ const activeOpponents = getActiveRoundPlayers(round).filter(
403
+ item => item.address !== botAddress
404
+ )
405
+
406
+ if (
407
+ allowedActions.includes('look') &&
408
+ !player?.looked &&
409
+ random() < (handStrength >= 0.68 ? 0.52 : 0.82)
410
+ ) {
411
+ return { action: 'look' }
412
+ }
413
+
414
+ if (
415
+ allowedActions.includes('compare') &&
416
+ activeOpponents.length === 1 &&
417
+ handStrength >= 0.54 &&
418
+ random() < 0.58
419
+ ) {
420
+ return { action: 'compare', target: activeOpponents[0].address }
421
+ }
422
+
423
+ if (allowedActions.includes('raise') && handStrength >= 0.7 && random() < 0.58) {
424
+ const amount = chooseBotRaiseAmount(round, botAddress, handStrength, random)
425
+ if (amount) return { action: 'raise', amount }
426
+ }
427
+
428
+ if (
429
+ allowedActions.includes('compare') &&
430
+ activeOpponents.length > 0 &&
431
+ handStrength >= 0.82 &&
432
+ random() < 0.36
433
+ ) {
434
+ return {
435
+ action: 'compare',
436
+ target: activeOpponents[Math.floor(random() * activeOpponents.length)].address,
437
+ }
438
+ }
439
+
440
+ if (
441
+ allowedActions.includes('call') &&
442
+ (handStrength >= 0.34 || random() < 0.62)
443
+ ) {
444
+ return { action: 'call' }
445
+ }
446
+
447
+ if (allowedActions.includes('fold')) return { action: 'fold' }
448
+ return { action: allowedActions[0] }
449
+ }
450
+
451
+ function payToCurrentBet(state, player) {
452
+ const need = Math.max(0, state.currentBet - player.bet)
453
+ player.chips -= need
454
+ player.bet += need
455
+ state.pot += need
456
+ }
457
+
458
+ function maybeFinishOrAdvance(state, fromAddress) {
459
+ const active = getActiveRoundPlayers(state)
460
+ if (active.length === 1) {
461
+ return finishRound(state, active[0].address)
462
+ }
463
+ state.turnAddress = nextActiveAddress(state, fromAddress)
464
+ return advanceSeq(state)
465
+ }
466
+
467
+ function finishRound(state, winnerAddress) {
468
+ const winner = state.players.find(player => player.address === winnerAddress)
469
+ if (winner) {
470
+ winner.chips += state.pot
471
+ }
472
+ state.status = 'finished'
473
+ state.winner = winnerAddress
474
+ state.turnAddress = ''
475
+ state.showdown = state.hands ? clone(state.hands) : null
476
+ state.lastAction = winner ? `${winner.name} 赢得 ${state.pot} 筹码` : '本局结束'
477
+ state.pot = 0
478
+ return advanceSeq(state)
479
+ }
480
+
481
+ function nextActiveAddress(state, fromAddress) {
482
+ const players = state.players
483
+ const start = Math.max(
484
+ 0,
485
+ players.findIndex(player => player.address === normalizeAddress(fromAddress))
486
+ )
487
+ for (let offset = 1; offset <= players.length; offset++) {
488
+ const player = players[(start + offset) % players.length]
489
+ if (player.status === 'active') return player.address
490
+ }
491
+ return ''
492
+ }
493
+
494
+ function advanceSeq(state) {
495
+ state.seq = Number(state.seq || 0) + 1
496
+ return { ok: true, state }
497
+ }
498
+
499
+ function estimateHandStrength(cards) {
500
+ try {
501
+ const result = evaluateHand(cards)
502
+ const categoryScore = (result.category - CATEGORY.high) / (CATEGORY.triple - CATEGORY.high)
503
+ const rankScore =
504
+ result.tiebreakers.reduce((sum, value, index) => {
505
+ return sum + (Number(value) || 0) / Math.pow(16, index + 1)
506
+ }, 0) / 1.05
507
+ return Math.max(0, Math.min(1, categoryScore * 0.72 + rankScore * 0.28))
508
+ } catch {
509
+ return 0.45
510
+ }
511
+ }
512
+
513
+ function chooseBotRaiseAmount(round, address, handStrength, random) {
514
+ const validAmounts = ZHJ_RAISE_STEPS.filter(amount =>
515
+ validatePlayerAction(round, { action: 'raise', amount }, address).ok
516
+ )
517
+ if (validAmounts.length === 0) return 0
518
+
519
+ const ceiling = handStrength >= 0.9 ? 100 : handStrength >= 0.8 ? 50 : 20
520
+ const pool = validAmounts.filter(amount => amount <= ceiling)
521
+ const choices = pool.length > 0 ? pool : validAmounts
522
+ return choices[Math.floor(random() * choices.length)]
523
+ }
524
+
525
+ function normalizeRoundPlayer(input) {
526
+ if (!input || typeof input !== 'object') return null
527
+ const address = normalizeAddress(input.address)
528
+ if (!address) return null
529
+ return {
530
+ address,
531
+ name: String(input.name || shortAddress(address)).slice(0, 50),
532
+ publicKey: String(input.publicKey || ''),
533
+ chips: Math.max(0, Number(input.chips) || 0),
534
+ status: input.status === 'folded' ? 'folded' : 'active',
535
+ looked: input.looked === true,
536
+ bet: Math.max(0, Number(input.bet) || 0),
537
+ }
538
+ }
539
+
540
+ function normalizeAddress(value) {
541
+ const address = String(value || '').trim()
542
+ return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''
543
+ }
544
+
545
+ function shortAddress(address) {
546
+ return address ? `${address.slice(0, 6)}...${address.slice(-4)}` : ''
547
+ }
548
+
549
+ function clone(value) {
550
+ return JSON.parse(JSON.stringify(value))
551
+ }
552
+
553
+ function randomHex(byteLength) {
554
+ const bytes = new Uint8Array(byteLength)
555
+ if (globalThis.crypto?.getRandomValues) {
556
+ globalThis.crypto.getRandomValues(bytes)
557
+ } else {
558
+ for (let i = 0; i < bytes.length; i++) {
559
+ bytes[i] = Math.floor(Math.random() * 256)
560
+ }
561
+ }
562
+ return [...bytes].map(byte => byte.toString(16).padStart(2, '0')).join('')
563
+ }