most-box 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +4 -4
- package/out/__next._full.txt +12 -12
- package/out/__next._head.txt +3 -3
- package/out/__next._index.txt +6 -6
- package/out/__next._tree.txt +1 -1
- package/out/_next/static/chunks/04mo7rr..0_1q.js +1 -0
- package/out/_next/static/chunks/06rf3qq5ggs6v.js +1 -0
- package/out/_next/static/chunks/{0aq.rc9woa2nz.js → 0_0oph_z1az14.js} +1 -1
- package/out/_next/static/chunks/0cl7d~7abnk_p.css +1 -0
- package/out/_next/static/chunks/0d306t1wvjpdx.js +1 -0
- package/out/_next/static/chunks/{0etes81d_cihn.js → 0m_5nb6x8qy._.js} +1 -1
- package/out/_next/static/chunks/0n.ayxmsar6e5.js +1 -0
- package/out/_next/static/chunks/0olqjomda37-e.js +1 -0
- package/out/_next/static/chunks/{16xls5tt_68lx.js → 0s~g.l~x049o2.js} +1 -1
- package/out/_next/static/chunks/0voe1.ttrh84k.css +1 -0
- package/out/_next/static/chunks/0x.ky97owcxxs.js +1 -0
- package/out/_next/static/chunks/0ysj5b94vu4ri.js +1 -0
- package/out/_next/static/chunks/{0q0ksgxg98xgd.js → 17cwkb2yn_akx.js} +1 -1
- package/out/_next/static/chunks/184hxsuf-5c84.js +1 -0
- 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/gandengyan/__next._full.txt +25 -0
- package/out/gandengyan/__next._head.txt +5 -0
- package/out/gandengyan/__next._index.txt +9 -0
- package/out/gandengyan/__next._tree.txt +5 -0
- package/out/gandengyan/__next.gandengyan.__PAGE__.txt +10 -0
- package/out/gandengyan/__next.gandengyan.txt +5 -0
- package/out/gandengyan/index.html +15 -0
- package/out/gandengyan/index.txt +25 -0
- package/out/index.html +2 -2
- package/out/index.txt +12 -12
- 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 +28 -29
- package/public/favicon.ico +0 -0
- package/server/index.js +8 -0
- package/server/src/games/gandengyan.js +675 -0
- package/server/src/http/access.js +4 -0
- package/server/src/http/app.js +7 -0
- 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/0gwian.hp3-92.js +0 -1
- package/out/_next/static/chunks/0l5_.uqb-uqb8.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/{t7ZIeQpVvjz4a7-5Tt-VK → aPEZ4zaaR5W3WpSZ0dFsa}/_buildManifest.js +0 -0
- /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → aPEZ4zaaR5W3WpSZ0dFsa}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{t7ZIeQpVvjz4a7-5Tt-VK → aPEZ4zaaR5W3WpSZ0dFsa}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const SUITS = ['S', 'H', 'C', 'D']
|
|
4
|
+
const RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2']
|
|
5
|
+
const STRAIGHT_RANKS = RANKS.filter(rank => rank !== '2')
|
|
6
|
+
const RANK_VALUE = new Map(RANKS.map((rank, index) => [rank, index + 3]))
|
|
7
|
+
const INITIAL_HAND_SIZE = 5
|
|
8
|
+
const SEALED_PENALTY = 15
|
|
9
|
+
|
|
10
|
+
export function createGanDengYanSocketHandlers() {
|
|
11
|
+
const rooms = new Map()
|
|
12
|
+
const clients = new Map()
|
|
13
|
+
|
|
14
|
+
function bindClient(ws) {
|
|
15
|
+
const id = crypto.randomUUID()
|
|
16
|
+
clients.set(ws, { id, roomId: null, name: '玩家', address: '' })
|
|
17
|
+
send(ws, 'gandengyan:hello', { playerId: id })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function unbindClient(ws) {
|
|
21
|
+
const client = clients.get(ws)
|
|
22
|
+
if (client?.roomId && rooms.has(client.roomId)) {
|
|
23
|
+
const room = rooms.get(client.roomId)
|
|
24
|
+
leaveRoom(room, client.id)
|
|
25
|
+
broadcastRoom(room)
|
|
26
|
+
}
|
|
27
|
+
clients.delete(ws)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function handleMessage(ws, event, data = {}) {
|
|
31
|
+
if (!event.startsWith('gandengyan:')) return false
|
|
32
|
+
const client = clients.get(ws)
|
|
33
|
+
if (!client) return true
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const identity = normalizeIdentity(data.identity)
|
|
37
|
+
if (identity.name) client.name = identity.name
|
|
38
|
+
if (identity.address) client.address = identity.address
|
|
39
|
+
|
|
40
|
+
switch (event) {
|
|
41
|
+
case 'gandengyan:createRoom': {
|
|
42
|
+
const room = createRoom({
|
|
43
|
+
roomId: makeRoomId(rooms),
|
|
44
|
+
ownerId: client.id,
|
|
45
|
+
ownerName: client.name,
|
|
46
|
+
ownerAddress: client.address,
|
|
47
|
+
})
|
|
48
|
+
rooms.set(room.id, room)
|
|
49
|
+
client.roomId = room.id
|
|
50
|
+
send(ws, 'gandengyan:roomCreated', { roomId: room.id })
|
|
51
|
+
broadcastRoom(room)
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
case 'gandengyan:joinRoom': {
|
|
55
|
+
const roomId = String(data.roomId || '').trim().toUpperCase()
|
|
56
|
+
const room = rooms.get(roomId)
|
|
57
|
+
if (!room) throw new Error('房间不存在')
|
|
58
|
+
joinRoom(room, client.id, client.name, client.address)
|
|
59
|
+
client.roomId = room.id
|
|
60
|
+
broadcastRoom(room)
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const room = rooms.get(client.roomId)
|
|
68
|
+
if (!room) throw new Error('请先进入房间')
|
|
69
|
+
|
|
70
|
+
if (event === 'gandengyan:settings') {
|
|
71
|
+
if (room.ownerId !== client.id) throw new Error('只有房主可以修改设置')
|
|
72
|
+
setRoomSettings(room, data.settings || {})
|
|
73
|
+
broadcastRoom(room)
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (event === 'gandengyan:start') {
|
|
78
|
+
if (room.ownerId !== client.id) throw new Error('只有房主可以开始')
|
|
79
|
+
startGame(room)
|
|
80
|
+
broadcastRoom(room)
|
|
81
|
+
scheduleBots(room, broadcastRoom)
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (event === 'gandengyan:play') {
|
|
86
|
+
playCards(room, client.id, Array.isArray(data.cardIds) ? data.cardIds : [])
|
|
87
|
+
broadcastRoom(room)
|
|
88
|
+
scheduleBots(room, broadcastRoom)
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (event === 'gandengyan:pass') {
|
|
93
|
+
passTurn(room, client.id)
|
|
94
|
+
broadcastRoom(room)
|
|
95
|
+
scheduleBots(room, broadcastRoom)
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (event === 'gandengyan:restart') {
|
|
100
|
+
if (room.ownerId !== client.id) throw new Error('只有房主可以再来一局')
|
|
101
|
+
startGame(room)
|
|
102
|
+
broadcastRoom(room)
|
|
103
|
+
scheduleBots(room, broadcastRoom)
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
send(ws, 'gandengyan:error', {
|
|
108
|
+
message: error instanceof Error ? error.message : '请求失败',
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function broadcastRoom(room) {
|
|
116
|
+
for (const [ws, client] of clients.entries()) {
|
|
117
|
+
if (client.roomId === room.id && ws.readyState === 1) {
|
|
118
|
+
send(ws, 'gandengyan:state', publicRoom(room, client.id))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { bindClient, unbindClient, handleMessage }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeIdentity(identity) {
|
|
127
|
+
if (!identity || typeof identity !== 'object') return {}
|
|
128
|
+
return {
|
|
129
|
+
name: cleanName(identity.displayName || identity.username || identity.name),
|
|
130
|
+
address: String(identity.address || '').trim().slice(0, 64),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function send(ws, event, data) {
|
|
135
|
+
if (ws.readyState === 1) {
|
|
136
|
+
ws.send(JSON.stringify({ event, data }))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function scheduleBots(room, broadcastRoom) {
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
let moved = false
|
|
143
|
+
let guard = 0
|
|
144
|
+
while (room.status === 'playing' && guard < 8 && botStep(room)) {
|
|
145
|
+
moved = true
|
|
146
|
+
guard += 1
|
|
147
|
+
}
|
|
148
|
+
if (moved) broadcastRoom(room)
|
|
149
|
+
}, 650)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createRoom({ roomId, ownerId, ownerName, ownerAddress }) {
|
|
153
|
+
const room = {
|
|
154
|
+
id: roomId,
|
|
155
|
+
ownerId,
|
|
156
|
+
status: 'lobby',
|
|
157
|
+
settings: { decks: 1, seats: 2, bots: 1 },
|
|
158
|
+
players: [
|
|
159
|
+
{
|
|
160
|
+
id: ownerId,
|
|
161
|
+
address: ownerAddress || '',
|
|
162
|
+
name: ownerName || '玩家',
|
|
163
|
+
bot: false,
|
|
164
|
+
seat: 0,
|
|
165
|
+
connected: true,
|
|
166
|
+
hand: [],
|
|
167
|
+
score: 0,
|
|
168
|
+
playedCards: 0,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
deck: [],
|
|
172
|
+
discard: [],
|
|
173
|
+
table: null,
|
|
174
|
+
currentSeat: 0,
|
|
175
|
+
lastWinnerSeat: null,
|
|
176
|
+
previousWinnerSeat: null,
|
|
177
|
+
passSeats: [],
|
|
178
|
+
baseScore: 1,
|
|
179
|
+
bombCount: 0,
|
|
180
|
+
diceRolls: [],
|
|
181
|
+
roundResult: null,
|
|
182
|
+
log: ['房间已创建'],
|
|
183
|
+
winnerSeat: null,
|
|
184
|
+
updatedAt: Date.now(),
|
|
185
|
+
}
|
|
186
|
+
syncBots(room)
|
|
187
|
+
return room
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function setRoomSettings(room, settings) {
|
|
191
|
+
if (room.status !== 'lobby') throw new Error('游戏开始后不能修改设置')
|
|
192
|
+
room.settings.decks = clamp(Number(settings.decks) || 1, 1, 2)
|
|
193
|
+
room.settings.seats = clamp(Number(settings.seats) || 2, 2, 6)
|
|
194
|
+
room.settings.bots = clamp(Number(settings.bots) || 0, 0, room.settings.seats - 1)
|
|
195
|
+
syncBots(room)
|
|
196
|
+
room.updatedAt = Date.now()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function joinRoom(room, playerId, name, address) {
|
|
200
|
+
let player = room.players.find(item => item.id === playerId)
|
|
201
|
+
if (player) {
|
|
202
|
+
player.connected = true
|
|
203
|
+
if (name) player.name = name
|
|
204
|
+
if (address) player.address = address
|
|
205
|
+
return player
|
|
206
|
+
}
|
|
207
|
+
if (room.status !== 'lobby') throw new Error('游戏已经开始')
|
|
208
|
+
const seat = firstOpenHumanSeat(room)
|
|
209
|
+
if (seat === -1) throw new Error('房间已满')
|
|
210
|
+
player = {
|
|
211
|
+
id: playerId,
|
|
212
|
+
address: address || '',
|
|
213
|
+
name: name || `玩家${seat + 1}`,
|
|
214
|
+
bot: false,
|
|
215
|
+
seat,
|
|
216
|
+
connected: true,
|
|
217
|
+
hand: [],
|
|
218
|
+
score: 0,
|
|
219
|
+
playedCards: 0,
|
|
220
|
+
}
|
|
221
|
+
room.players = room.players.filter(item => item.seat !== seat)
|
|
222
|
+
room.players.push(player)
|
|
223
|
+
room.players.sort((a, b) => a.seat - b.seat)
|
|
224
|
+
syncBots(room)
|
|
225
|
+
room.log.unshift(`${player.name} 加入牌桌`)
|
|
226
|
+
room.updatedAt = Date.now()
|
|
227
|
+
return player
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function leaveRoom(room, playerId) {
|
|
231
|
+
const player = room.players.find(item => item.id === playerId)
|
|
232
|
+
if (!player || player.bot) return
|
|
233
|
+
player.connected = false
|
|
234
|
+
if (room.status === 'lobby') {
|
|
235
|
+
room.players = room.players.filter(item => item.id !== playerId)
|
|
236
|
+
syncBots(room)
|
|
237
|
+
}
|
|
238
|
+
room.updatedAt = Date.now()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function startGame(room) {
|
|
242
|
+
if (room.players.filter(player => !player.bot).length < 1) {
|
|
243
|
+
throw new Error('至少需要 1 名真人玩家')
|
|
244
|
+
}
|
|
245
|
+
syncBots(room)
|
|
246
|
+
room.deck = shuffle(createDeck(room.settings.decks))
|
|
247
|
+
room.discard = []
|
|
248
|
+
room.table = null
|
|
249
|
+
room.passSeats = []
|
|
250
|
+
room.winnerSeat = null
|
|
251
|
+
room.roundResult = null
|
|
252
|
+
room.baseScore = 1
|
|
253
|
+
room.bombCount = 0
|
|
254
|
+
room.status = 'playing'
|
|
255
|
+
for (const player of orderedPlayers(room)) {
|
|
256
|
+
player.hand = draw(room, INITIAL_HAND_SIZE)
|
|
257
|
+
player.playedCards = 0
|
|
258
|
+
sortHand(player.hand)
|
|
259
|
+
}
|
|
260
|
+
const starter = chooseStarter(room)
|
|
261
|
+
room.currentSeat = starter.seat
|
|
262
|
+
room.lastWinnerSeat = null
|
|
263
|
+
room.log = [`新一局开始,${starter.name} 先出牌`]
|
|
264
|
+
if (room.diceRolls.length > 0) {
|
|
265
|
+
room.log.unshift(`骰子结果:${room.diceRolls.map(roll => `${roll.name} ${roll.value}`).join(',')}`)
|
|
266
|
+
}
|
|
267
|
+
room.updatedAt = Date.now()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function playCards(room, playerId, cardIds) {
|
|
271
|
+
const player = currentPlayer(room)
|
|
272
|
+
if (!player || player.id !== playerId) throw new Error('还没轮到你')
|
|
273
|
+
const cards = cardIds.map(id => player.hand.find(card => card.id === id))
|
|
274
|
+
if (cards.length === 0 || cards.some(card => !card)) throw new Error('手牌不存在')
|
|
275
|
+
const combo = analyzeCards(cards)
|
|
276
|
+
if (!combo) throw new Error('这个牌型不合法')
|
|
277
|
+
if (!canBeat(combo, room.table?.combo)) throw new Error('出的牌压不过上一手')
|
|
278
|
+
player.hand = player.hand.filter(card => !cardIds.includes(card.id))
|
|
279
|
+
player.playedCards += cards.length
|
|
280
|
+
room.discard.push(...cards)
|
|
281
|
+
room.table = { seat: player.seat, playerName: player.name, cards, combo }
|
|
282
|
+
room.passSeats = []
|
|
283
|
+
room.lastWinnerSeat = player.seat
|
|
284
|
+
if (combo.type === 'bomb') {
|
|
285
|
+
room.bombCount += 1
|
|
286
|
+
room.baseScore *= 2
|
|
287
|
+
}
|
|
288
|
+
room.log.unshift(`${player.name} 出 ${combo.label} ${cards.map(labelCard).join(' ')}${combo.type === 'bomb' ? `,底分 ${room.baseScore}` : ''}`)
|
|
289
|
+
if (player.hand.length === 0) {
|
|
290
|
+
finishGame(room, player)
|
|
291
|
+
} else {
|
|
292
|
+
advanceTurn(room)
|
|
293
|
+
}
|
|
294
|
+
room.updatedAt = Date.now()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function passTurn(room, playerId) {
|
|
298
|
+
const player = currentPlayer(room)
|
|
299
|
+
if (!player || player.id !== playerId) throw new Error('还没轮到你')
|
|
300
|
+
if (!room.table) throw new Error('领出时不能不要')
|
|
301
|
+
if (!room.passSeats.includes(player.seat)) room.passSeats.push(player.seat)
|
|
302
|
+
room.log.unshift(`${player.name} 不要`)
|
|
303
|
+
const activeSeats = activePlayers(room).map(item => item.seat)
|
|
304
|
+
const seatsToBeat = activeSeats.filter(seat => seat !== room.lastWinnerSeat)
|
|
305
|
+
if (seatsToBeat.every(seat => room.passSeats.includes(seat))) {
|
|
306
|
+
refillAfterRound(room)
|
|
307
|
+
room.currentSeat = room.lastWinnerSeat
|
|
308
|
+
room.table = null
|
|
309
|
+
room.passSeats = []
|
|
310
|
+
room.log.unshift('本轮结束,所有玩家各补 1 张,重新领出')
|
|
311
|
+
} else {
|
|
312
|
+
advanceTurn(room)
|
|
313
|
+
}
|
|
314
|
+
room.updatedAt = Date.now()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function botStep(room) {
|
|
318
|
+
const player = currentPlayer(room)
|
|
319
|
+
if (!player?.bot || room.status !== 'playing') return false
|
|
320
|
+
const move = chooseBotMove(player.hand, room.table?.combo)
|
|
321
|
+
if (move.length > 0) {
|
|
322
|
+
playCards(room, player.id, move.map(card => card.id))
|
|
323
|
+
} else {
|
|
324
|
+
passTurn(room, player.id)
|
|
325
|
+
}
|
|
326
|
+
return true
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function publicRoom(room, viewerId) {
|
|
330
|
+
return {
|
|
331
|
+
id: room.id,
|
|
332
|
+
ownerId: room.ownerId,
|
|
333
|
+
status: room.status,
|
|
334
|
+
settings: room.settings,
|
|
335
|
+
deckCount: room.deck.length,
|
|
336
|
+
discardCount: room.discard.length,
|
|
337
|
+
currentSeat: room.currentSeat,
|
|
338
|
+
lastWinnerSeat: room.lastWinnerSeat,
|
|
339
|
+
previousWinnerSeat: room.previousWinnerSeat,
|
|
340
|
+
baseScore: room.baseScore,
|
|
341
|
+
bombCount: room.bombCount,
|
|
342
|
+
diceRolls: room.diceRolls,
|
|
343
|
+
roundResult: room.roundResult,
|
|
344
|
+
table: room.table ? { ...room.table, cards: room.table.cards.map(publicCard) } : null,
|
|
345
|
+
passSeats: room.passSeats,
|
|
346
|
+
winnerSeat: room.winnerSeat,
|
|
347
|
+
log: room.log.slice(0, 18),
|
|
348
|
+
players: orderedPlayers(room).map(player => ({
|
|
349
|
+
id: player.id,
|
|
350
|
+
address: player.address,
|
|
351
|
+
name: player.name,
|
|
352
|
+
bot: player.bot,
|
|
353
|
+
seat: player.seat,
|
|
354
|
+
connected: player.connected,
|
|
355
|
+
handCount: player.hand.length,
|
|
356
|
+
score: player.score,
|
|
357
|
+
playedCards: player.playedCards,
|
|
358
|
+
hand: player.id === viewerId ? player.hand.map(publicCard) : [],
|
|
359
|
+
})),
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function analyzeCards(cards) {
|
|
364
|
+
if (!cards?.length) return null
|
|
365
|
+
const jokerCount = cards.filter(isJoker).length
|
|
366
|
+
const normals = cards.filter(card => !isJoker(card))
|
|
367
|
+
if (normals.length === 0) return null
|
|
368
|
+
const bomb = analyzeBomb(cards, normals, jokerCount)
|
|
369
|
+
if (bomb) return bomb
|
|
370
|
+
if (cards.length === 1 && jokerCount === 0) return makeCombo('single', cardValue(cards[0]), 1, [cardValue(cards[0])])
|
|
371
|
+
if (cards.length === 2 && canRepresentSameRank(normals, jokerCount)) {
|
|
372
|
+
const value = sameRankValue(normals)
|
|
373
|
+
return makeCombo('pair', value, 2, [value, value])
|
|
374
|
+
}
|
|
375
|
+
return analyzeStraight(cards) || analyzePairStraight(cards)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function analyzeBomb(cards, normals, jokerCount) {
|
|
379
|
+
if (cards.length < 3 || !canRepresentSameRank(normals, jokerCount)) return null
|
|
380
|
+
const value = sameRankValue(normals)
|
|
381
|
+
return makeCombo('bomb', value, cards.length, Array(cards.length).fill(value), jokerCount === 0)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function analyzeStraight(cards) {
|
|
385
|
+
if (cards.length < 3) return null
|
|
386
|
+
for (let start = 0; start <= STRAIGHT_RANKS.length - cards.length; start += 1) {
|
|
387
|
+
const values = STRAIGHT_RANKS.slice(start, start + cards.length).map(rank => RANK_VALUE.get(rank))
|
|
388
|
+
if (cards.every((card, index) => isJoker(card) || cardValue(card) === values[index])) {
|
|
389
|
+
return makeCombo('straight', values.at(-1), cards.length, values)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return null
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function analyzePairStraight(cards) {
|
|
396
|
+
if (cards.length < 4 || cards.length % 2 !== 0) return null
|
|
397
|
+
const pairCount = cards.length / 2
|
|
398
|
+
for (let start = 0; start <= STRAIGHT_RANKS.length - pairCount; start += 1) {
|
|
399
|
+
const pairValues = STRAIGHT_RANKS.slice(start, start + pairCount).map(rank => RANK_VALUE.get(rank))
|
|
400
|
+
const values = pairValues.flatMap(value => [value, value])
|
|
401
|
+
if (cards.every((card, index) => isJoker(card) || cardValue(card) === values[index])) {
|
|
402
|
+
return makeCombo('pairStraight', pairValues.at(-1), cards.length, values)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function makeCombo(type, value, length, resolvedValues, pure = true) {
|
|
409
|
+
return { type, value, length, resolvedValues, pure, label: labelCombo({ type, value, length, resolvedValues, pure }) }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function canBeat(combo, tableCombo) {
|
|
413
|
+
if (!combo) return false
|
|
414
|
+
if (!tableCombo) return true
|
|
415
|
+
if (combo.type === 'bomb' && tableCombo.type !== 'bomb') return true
|
|
416
|
+
if (combo.type !== tableCombo.type) return false
|
|
417
|
+
if (combo.type === 'bomb') {
|
|
418
|
+
if (combo.length !== tableCombo.length) return combo.length > tableCombo.length
|
|
419
|
+
if (combo.value !== tableCombo.value) return combo.value > tableCombo.value
|
|
420
|
+
return combo.pure && !tableCombo.pure
|
|
421
|
+
}
|
|
422
|
+
if (combo.length !== tableCombo.length) return false
|
|
423
|
+
if (combo.type === 'single' || combo.type === 'pair') {
|
|
424
|
+
return combo.value === nextValue(tableCombo.value) || combo.value === RANK_VALUE.get('2')
|
|
425
|
+
}
|
|
426
|
+
return combo.value === nextValue(tableCombo.value)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function syncBots(room) {
|
|
430
|
+
const seats = room.settings.seats
|
|
431
|
+
room.players = room.players.filter(player => player.seat < seats && (!player.bot || room.status === 'lobby'))
|
|
432
|
+
while (orderedPlayers(room).filter(player => player.bot).length > room.settings.bots) {
|
|
433
|
+
const bot = orderedPlayers(room).filter(player => player.bot).at(-1)
|
|
434
|
+
room.players = room.players.filter(player => player.id !== bot.id)
|
|
435
|
+
}
|
|
436
|
+
for (let seat = 0; orderedPlayers(room).filter(player => player.bot).length < room.settings.bots && seat < seats; seat += 1) {
|
|
437
|
+
if (!room.players.find(player => player.seat === seat)) room.players.push(makeBot(seat))
|
|
438
|
+
}
|
|
439
|
+
room.players.sort((a, b) => a.seat - b.seat)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function makeBot(seat) {
|
|
443
|
+
return {
|
|
444
|
+
id: `bot-${seat}-${Math.random().toString(36).slice(2, 8)}`,
|
|
445
|
+
address: '',
|
|
446
|
+
name: `人机${seat + 1}`,
|
|
447
|
+
bot: true,
|
|
448
|
+
seat,
|
|
449
|
+
connected: true,
|
|
450
|
+
hand: [],
|
|
451
|
+
score: 0,
|
|
452
|
+
playedCards: 0,
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function chooseStarter(room) {
|
|
457
|
+
if (room.previousWinnerSeat !== null) {
|
|
458
|
+
room.diceRolls = []
|
|
459
|
+
return orderedPlayers(room).find(player => player.seat === room.previousWinnerSeat) || orderedPlayers(room)[0]
|
|
460
|
+
}
|
|
461
|
+
let candidates = orderedPlayers(room)
|
|
462
|
+
let rolls = []
|
|
463
|
+
while (candidates.length > 1) {
|
|
464
|
+
rolls = candidates.map(player => ({ seat: player.seat, name: player.name, value: rollDice() }))
|
|
465
|
+
const max = Math.max(...rolls.map(roll => roll.value))
|
|
466
|
+
const winners = rolls.filter(roll => roll.value === max)
|
|
467
|
+
if (winners.length === 1) {
|
|
468
|
+
room.diceRolls = rolls
|
|
469
|
+
return candidates.find(player => player.seat === winners[0].seat)
|
|
470
|
+
}
|
|
471
|
+
candidates = candidates.filter(player => winners.some(roll => roll.seat === player.seat))
|
|
472
|
+
}
|
|
473
|
+
room.diceRolls = rolls
|
|
474
|
+
return candidates[0]
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function finishGame(room, winner) {
|
|
478
|
+
room.status = 'finished'
|
|
479
|
+
room.winnerSeat = winner.seat
|
|
480
|
+
room.previousWinnerSeat = winner.seat
|
|
481
|
+
const losers = []
|
|
482
|
+
let winnerGain = 0
|
|
483
|
+
for (const player of orderedPlayers(room)) {
|
|
484
|
+
if (player.seat === winner.seat) continue
|
|
485
|
+
const sealed = player.playedCards === 0
|
|
486
|
+
const loss = sealed ? SEALED_PENALTY : player.hand.length * room.baseScore
|
|
487
|
+
player.score -= loss
|
|
488
|
+
winnerGain += loss
|
|
489
|
+
losers.push({ seat: player.seat, name: player.name, loss, sealed, cardsLeft: player.hand.length })
|
|
490
|
+
}
|
|
491
|
+
winner.score += winnerGain
|
|
492
|
+
room.roundResult = { winnerSeat: winner.seat, winnerName: winner.name, winnerGain, baseScore: room.baseScore, bombCount: room.bombCount, losers }
|
|
493
|
+
room.log.unshift(`${winner.name} 获胜,赢 ${winnerGain} 分`)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function refillAfterRound(room) {
|
|
497
|
+
for (const player of activePlayers(room)) {
|
|
498
|
+
if (room.deck.length === 0) break
|
|
499
|
+
player.hand.push(...draw(room, 1))
|
|
500
|
+
sortHand(player.hand)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function firstOpenHumanSeat(room) {
|
|
505
|
+
for (let seat = 0; seat < room.settings.seats; seat += 1) {
|
|
506
|
+
const player = room.players.find(item => item.seat === seat)
|
|
507
|
+
if (!player || player.bot) return seat
|
|
508
|
+
}
|
|
509
|
+
return -1
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function createDeck(decks) {
|
|
513
|
+
const cards = []
|
|
514
|
+
for (let deck = 0; deck < decks; deck += 1) {
|
|
515
|
+
for (const suit of SUITS) {
|
|
516
|
+
for (const rank of RANKS) cards.push({ id: `${deck}-${suit}-${rank}`, suit, rank, deck })
|
|
517
|
+
}
|
|
518
|
+
cards.push({ id: `${deck}-SJ`, suit: 'Joker', rank: 'SJ', deck })
|
|
519
|
+
cards.push({ id: `${deck}-BJ`, suit: 'Joker', rank: 'BJ', deck })
|
|
520
|
+
}
|
|
521
|
+
return cards
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function shuffle(cards) {
|
|
525
|
+
const copy = [...cards]
|
|
526
|
+
for (let index = copy.length - 1; index > 0; index -= 1) {
|
|
527
|
+
const swapIndex = Math.floor(Math.random() * (index + 1))
|
|
528
|
+
;[copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]]
|
|
529
|
+
}
|
|
530
|
+
return copy
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function draw(room, count) {
|
|
534
|
+
return room.deck.splice(0, count)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function sortHand(hand) {
|
|
538
|
+
hand.sort(compareCards)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function currentPlayer(room) {
|
|
542
|
+
return room.players.find(player => player.seat === room.currentSeat)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function activePlayers(room) {
|
|
546
|
+
return orderedPlayers(room).filter(player => player.hand.length > 0)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function orderedPlayers(room) {
|
|
550
|
+
return [...room.players].sort((a, b) => a.seat - b.seat)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function advanceTurn(room) {
|
|
554
|
+
const seats = activePlayers(room).map(player => player.seat)
|
|
555
|
+
const currentIndex = seats.indexOf(room.currentSeat)
|
|
556
|
+
room.currentSeat = seats[(currentIndex + 1) % seats.length]
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function chooseBotMove(hand, tableCombo) {
|
|
560
|
+
const candidates = enumerateBotMoves(hand)
|
|
561
|
+
return candidates.find(cards => canBeat(analyzeCards(cards), tableCombo)) || []
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function enumerateBotMoves(hand) {
|
|
565
|
+
const sorted = [...hand].sort(compareCards)
|
|
566
|
+
const moves = sorted.filter(card => !isJoker(card)).map(card => [card])
|
|
567
|
+
for (let size = 2; size <= Math.min(6, sorted.length); size += 1) {
|
|
568
|
+
for (const cards of combinations(sorted, size)) {
|
|
569
|
+
if (analyzeCards(cards)) moves.push(cards)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return moves.sort((a, b) => {
|
|
573
|
+
const comboA = analyzeCards(a)
|
|
574
|
+
const comboB = analyzeCards(b)
|
|
575
|
+
if (comboA.type === 'bomb' && comboB.type !== 'bomb') return 1
|
|
576
|
+
if (comboA.type !== 'bomb' && comboB.type === 'bomb') return -1
|
|
577
|
+
return comboA.length - comboB.length || comboA.value - comboB.value
|
|
578
|
+
})
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function combinations(cards, size, start = 0, prefix = [], output = []) {
|
|
582
|
+
if (prefix.length === size) {
|
|
583
|
+
output.push(prefix)
|
|
584
|
+
return output
|
|
585
|
+
}
|
|
586
|
+
for (let index = start; index <= cards.length - (size - prefix.length); index += 1) {
|
|
587
|
+
combinations(cards, size, index + 1, [...prefix, cards[index]], output)
|
|
588
|
+
}
|
|
589
|
+
return output
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function canRepresentSameRank(normals, jokerCount) {
|
|
593
|
+
if (normals.length === 0) return false
|
|
594
|
+
const first = normals[0].rank
|
|
595
|
+
return normals.every(card => card.rank === first) && normals.length + jokerCount >= 2
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function sameRankValue(normals) {
|
|
599
|
+
return cardValue(normals[0])
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function compareCards(a, b) {
|
|
603
|
+
return cardValue(a) - cardValue(b) || suitValue(a.suit) - suitValue(b.suit)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function cardValue(card) {
|
|
607
|
+
return RANK_VALUE.get(card.rank) || 99
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function isJoker(card) {
|
|
611
|
+
return card.rank === 'SJ' || card.rank === 'BJ'
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function suitValue(suit) {
|
|
615
|
+
return { D: 0, C: 1, H: 2, S: 3, Joker: 4 }[suit] || 0
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function publicCard(card) {
|
|
619
|
+
return { id: card.id, suit: card.suit, rank: card.rank, label: labelCard(card), color: cardColor(card) }
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function labelCard(card) {
|
|
623
|
+
if (card.rank === 'SJ') return '小王'
|
|
624
|
+
if (card.rank === 'BJ') return '大王'
|
|
625
|
+
return `${suitSymbol(card.suit)}${card.rank}`
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function suitSymbol(suit) {
|
|
629
|
+
return { S: '♠', H: '♥', C: '♣', D: '♦' }[suit] || ''
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function cardColor(card) {
|
|
633
|
+
return card.suit === 'H' || card.suit === 'D' || card.rank === 'BJ' ? 'red' : 'black'
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function labelCombo(combo) {
|
|
637
|
+
const resolved = combo.resolvedValues?.length ? `(${combo.resolvedValues.map(valueLabel).join(' ')})` : ''
|
|
638
|
+
return `${{
|
|
639
|
+
single: '单张',
|
|
640
|
+
pair: '对子',
|
|
641
|
+
straight: '顺子',
|
|
642
|
+
pairStraight: '连对',
|
|
643
|
+
bomb: combo.pure ? '纯炸弹' : '带王炸弹',
|
|
644
|
+
}[combo.type]}${resolved}`
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function valueLabel(value) {
|
|
648
|
+
return RANKS.find(rank => RANK_VALUE.get(rank) === value) || String(value)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function nextValue(value) {
|
|
652
|
+
const index = RANKS.findIndex(rank => RANK_VALUE.get(rank) === value)
|
|
653
|
+
if (index === -1 || index >= RANKS.length - 1) return null
|
|
654
|
+
return RANK_VALUE.get(RANKS[index + 1])
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function makeRoomId(rooms) {
|
|
658
|
+
let id = ''
|
|
659
|
+
do {
|
|
660
|
+
id = Math.random().toString(36).slice(2, 8).toUpperCase()
|
|
661
|
+
} while (rooms.has(id))
|
|
662
|
+
return id
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function cleanName(name) {
|
|
666
|
+
return String(name || '玩家').trim().slice(0, 16) || '玩家'
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function rollDice() {
|
|
670
|
+
return Math.floor(Math.random() * 6) + 1
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function clamp(value, min, max) {
|
|
674
|
+
return Math.max(min, Math.min(max, value))
|
|
675
|
+
}
|
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/*',
|