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.
- package/README.md +18 -5
- package/electron/afterPack.cjs +87 -0
- package/electron/main.js +85 -1
- package/electron/updateChecker.js +97 -0
- package/electron/updateChecker.test.js +147 -0
- package/out/404/index.html +2 -2
- package/out/404.html +2 -2
- package/out/__next.__PAGE__.txt +5 -5
- package/out/__next._full.txt +13 -13
- package/out/__next._head.txt +3 -3
- package/out/__next._index.txt +6 -6
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/chunks/02zzxfop_k6tl.css +1 -0
- package/out/_next/static/chunks/03gsbg0fr00ey.js +1 -0
- package/out/_next/static/chunks/06lttvu7563zo.css +1 -0
- package/out/_next/static/chunks/0fo9-h4knidcz.js +1 -0
- package/out/_next/static/chunks/0hyds~bp.auvh.js +1 -0
- package/out/_next/static/chunks/0i9sfdypwuw8~.js +1 -0
- package/out/_next/static/chunks/0olqjomda37-e.js +1 -0
- package/out/_next/static/chunks/{0aq.rc9woa2nz.js → 0puk.7e.tr2zy.js} +1 -1
- package/out/_next/static/chunks/0s1k6rlwy02c2.js +1 -0
- package/out/_next/static/chunks/0sgltmtk_9s8p.css +1 -0
- package/out/_next/static/chunks/0snehvtvu1n4q.js +1 -0
- package/out/_next/static/chunks/{16xls5tt_68lx.js → 0s~g.l~x049o2.js} +1 -1
- package/out/_next/static/chunks/{12nr19.nnn6s3.js → 0t_3xxx4zkerp.js} +2 -2
- package/out/_next/static/chunks/0u38kke9vhobe.js +1 -0
- package/out/_next/static/chunks/0vd4_a5x-wpdh.js +1 -0
- package/out/_next/static/chunks/0xx_10jns1.s7.css +1 -0
- package/out/_next/static/chunks/{0etes81d_cihn.js → 10f-t2n4y1zx8.js} +1 -1
- package/out/_next/static/chunks/{0l5_.uqb-uqb8.js → 13jdyag9a-~kk.js} +1 -1
- package/out/_next/static/chunks/{0q0ksgxg98xgd.js → 17cwkb2yn_akx.js} +1 -1
- package/out/_not-found/__next._full.txt +11 -11
- package/out/_not-found/__next._head.txt +3 -3
- package/out/_not-found/__next._index.txt +6 -6
- package/out/_not-found/__next._not-found.__PAGE__.txt +4 -4
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +1 -1
- package/out/_not-found/index.html +2 -2
- package/out/_not-found/index.txt +11 -11
- package/out/admin/__next._full.txt +12 -12
- package/out/admin/__next._head.txt +3 -3
- package/out/admin/__next._index.txt +6 -6
- package/out/admin/__next._tree.txt +1 -1
- package/out/admin/__next.admin.__PAGE__.txt +4 -4
- package/out/admin/__next.admin.txt +3 -3
- package/out/admin/index.html +2 -2
- package/out/admin/index.txt +12 -12
- package/out/app/__next._full.txt +12 -12
- package/out/app/__next._head.txt +3 -3
- package/out/app/__next._index.txt +6 -6
- package/out/app/__next._tree.txt +1 -1
- package/out/app/__next.app.__PAGE__.txt +4 -4
- package/out/app/__next.app.txt +3 -3
- package/out/app/index.html +2 -2
- package/out/app/index.txt +12 -12
- package/out/chat/__next._full.txt +12 -12
- package/out/chat/__next._head.txt +3 -3
- package/out/chat/__next._index.txt +6 -6
- package/out/chat/__next._tree.txt +1 -1
- package/out/chat/__next.chat.__PAGE__.txt +4 -4
- package/out/chat/__next.chat.txt +3 -3
- package/out/chat/index.html +2 -2
- package/out/chat/index.txt +12 -12
- package/out/chat/join/__next._full.txt +12 -12
- package/out/chat/join/__next._head.txt +3 -3
- package/out/chat/join/__next._index.txt +6 -6
- package/out/chat/join/__next._tree.txt +1 -1
- package/out/chat/join/__next.chat.join.__PAGE__.txt +4 -4
- package/out/chat/join/__next.chat.join.txt +3 -3
- package/out/chat/join/__next.chat.txt +3 -3
- package/out/chat/join/index.html +2 -2
- package/out/chat/join/index.txt +12 -12
- package/out/download/__next._full.txt +34 -30
- package/out/download/__next._head.txt +3 -3
- package/out/download/__next._index.txt +6 -6
- package/out/download/__next._tree.txt +2 -2
- package/out/download/__next.download.__PAGE__.txt +8 -13
- package/out/download/__next.download.txt +3 -3
- package/out/download/index.html +2 -2
- package/out/download/index.txt +34 -30
- package/out/favicon.ico +0 -0
- package/out/game/__next._full.txt +20 -0
- package/out/game/__next._head.txt +5 -0
- package/out/game/__next._index.txt +9 -0
- package/out/game/__next._tree.txt +5 -0
- package/out/game/__next.game.__PAGE__.txt +6 -0
- package/out/game/__next.game.txt +5 -0
- package/out/game/gandengyan/__next._full.txt +26 -0
- package/out/game/gandengyan/__next._head.txt +5 -0
- package/out/game/gandengyan/__next._index.txt +9 -0
- package/out/game/gandengyan/__next._tree.txt +6 -0
- package/out/game/gandengyan/__next.game.gandengyan.__PAGE__.txt +10 -0
- package/out/game/gandengyan/__next.game.gandengyan.txt +5 -0
- package/out/game/gandengyan/__next.game.txt +5 -0
- package/out/game/gandengyan/index.html +15 -0
- package/out/game/gandengyan/index.txt +26 -0
- package/out/game/index.html +1 -0
- package/out/game/index.txt +20 -0
- package/out/game/zhajinhua/__next._full.txt +25 -0
- package/out/game/zhajinhua/__next._head.txt +5 -0
- package/out/game/zhajinhua/__next._index.txt +9 -0
- package/out/game/zhajinhua/__next._tree.txt +5 -0
- package/out/game/zhajinhua/__next.game.txt +5 -0
- package/out/game/zhajinhua/__next.game.zhajinhua.__PAGE__.txt +9 -0
- package/out/game/zhajinhua/__next.game.zhajinhua.txt +5 -0
- package/out/game/zhajinhua/index.html +15 -0
- package/out/game/zhajinhua/index.txt +25 -0
- package/out/index.html +2 -2
- package/out/index.txt +13 -13
- package/out/note/__next._full.txt +12 -12
- package/out/note/__next._head.txt +3 -3
- package/out/note/__next._index.txt +6 -6
- package/out/note/__next._tree.txt +1 -1
- package/out/note/__next.note.__PAGE__.txt +4 -4
- package/out/note/__next.note.txt +3 -3
- package/out/note/index.html +2 -2
- package/out/note/index.txt +12 -12
- package/out/ping/__next._full.txt +12 -12
- package/out/ping/__next._head.txt +3 -3
- package/out/ping/__next._index.txt +6 -6
- package/out/ping/__next._tree.txt +1 -1
- package/out/ping/__next.ping.__PAGE__.txt +4 -4
- package/out/ping/__next.ping.txt +3 -3
- package/out/ping/index.html +2 -2
- package/out/ping/index.txt +12 -12
- package/out/web3/__next._full.txt +12 -12
- package/out/web3/__next._head.txt +3 -3
- package/out/web3/__next._index.txt +6 -6
- package/out/web3/__next._tree.txt +1 -1
- package/out/web3/__next.web3.__PAGE__.txt +4 -4
- package/out/web3/__next.web3.txt +3 -3
- package/out/web3/ed25519/__next._full.txt +10 -10
- package/out/web3/ed25519/__next._head.txt +3 -3
- package/out/web3/ed25519/__next._index.txt +6 -6
- package/out/web3/ed25519/__next._tree.txt +1 -1
- package/out/web3/ed25519/__next.web3.ed25519.__PAGE__.txt +2 -2
- package/out/web3/ed25519/__next.web3.ed25519.txt +3 -3
- package/out/web3/ed25519/__next.web3.txt +3 -3
- package/out/web3/ed25519/index.html +1 -1
- package/out/web3/ed25519/index.txt +10 -10
- package/out/web3/index.html +2 -2
- package/out/web3/index.txt +12 -12
- package/out/web3/tools/__next._full.txt +10 -10
- package/out/web3/tools/__next._head.txt +3 -3
- package/out/web3/tools/__next._index.txt +6 -6
- package/out/web3/tools/__next._tree.txt +1 -1
- package/out/web3/tools/__next.web3.tools.__PAGE__.txt +2 -2
- package/out/web3/tools/__next.web3.tools.txt +3 -3
- package/out/web3/tools/__next.web3.txt +3 -3
- package/out/web3/tools/index.html +1 -1
- package/out/web3/tools/index.txt +10 -10
- package/package.json +29 -29
- package/public/favicon.ico +0 -0
- package/server/src/config.js +1 -1
- package/server/src/core/gameRoom.js +222 -0
- package/server/src/core/zhajinhua.js +563 -0
- package/server/src/games/gandengyan.js +612 -0
- package/server/src/http/access.js +4 -0
- package/server/src/http/app.js +14 -1
- package/server/src/index.js +9 -1
- package/server/src/utils/api.js +19 -1
- package/out/_next/static/chunks/0.e2avjgna_b2.js +0 -1
- package/out/_next/static/chunks/03h~nhgj0hv3p.css +0 -1
- package/out/_next/static/chunks/07td.jq7xff84.css +0 -1
- package/out/_next/static/chunks/0gwian.hp3-92.js +0 -1
- package/out/_next/static/chunks/0mex8svsiv-2l.js +0 -1
- package/out/_next/static/chunks/0myq9gs8szydh.js +0 -1
- package/out/_next/static/chunks/0p0sv~fuddvgr.js +0 -1
- package/out/_next/static/chunks/0wtf0xsiicxx6.js +0 -1
- /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → sSvbBrwXZY-4lBmcHshga}/_buildManifest.js +0 -0
- /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → sSvbBrwXZY-4lBmcHshga}/_clientMiddlewareManifest.js +0 -0
- /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
|
+
}
|