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,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
|
+
}
|
package/server/src/http/app.js
CHANGED
|
@@ -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(
|
|
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 => {
|
package/server/src/index.js
CHANGED
|
@@ -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,
|