most-box 0.1.3 → 0.1.5
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 +7 -4
- package/electron/main.js +31 -10
- package/out/404/index.html +2 -2
- package/out/404.html +2 -2
- package/out/__next.__PAGE__.txt +6 -6
- package/out/__next._full.txt +15 -15
- package/out/__next._head.txt +3 -3
- package/out/__next._index.txt +7 -7
- package/out/__next._tree.txt +4 -4
- package/out/_next/static/chunks/{0hpev4am9jpmu.css → 0.4ov9319fecg.css} +1 -1
- package/out/_next/static/chunks/0._8s6pbfw.xk.js +1 -0
- package/out/_next/static/chunks/02jvyg27pp0mz.js +1 -0
- package/out/_next/static/chunks/02zzxfop_k6tl.css +1 -0
- package/out/_next/static/chunks/06lttvu7563zo.css +1 -0
- package/out/_next/static/chunks/{0pt.5cg1t09qs.js → 08qqf0rsi3oot.js} +1 -1
- package/out/_next/static/chunks/{0adx~d-j05c9d.css → 09h7l4i38xc7q.css} +1 -1
- package/out/_next/static/chunks/09hqx79-jkvm_.css +1 -0
- package/out/_next/static/chunks/09vcm1ku9k7o8.js +1 -0
- package/out/_next/static/chunks/0c6e_d2jq179x.js +1 -0
- package/out/_next/static/chunks/0ccalho416.d7.js +1 -0
- package/out/_next/static/chunks/0exj_tg.ew-t3.js +1 -0
- package/out/_next/static/chunks/0f-wz5d~tv-r4.js +1 -0
- package/out/_next/static/chunks/0f0jhsujtf-61.js +1 -0
- package/out/_next/static/chunks/0hyds~bp.auvh.js +1 -0
- package/out/_next/static/chunks/0ig8a4sazk3~2.css +1 -0
- package/out/_next/static/chunks/0iq1h7g4dudg8.js +1 -0
- package/out/_next/static/chunks/0knnzo9aih48r.js +1 -0
- package/out/_next/static/chunks/0nzyk~sg_tn._.js +1 -0
- package/out/_next/static/chunks/{12nr19.nnn6s3.js → 0t_3xxx4zkerp.js} +2 -2
- package/out/_next/static/chunks/0w~2fjq86t7c7.css +1 -0
- package/out/_next/static/chunks/0xrqerhosrp9~.js +1 -0
- package/out/_next/static/chunks/0xx_10jns1.s7.css +1 -0
- package/out/_next/static/chunks/14-fm9r_mom81.js +1 -0
- package/out/_next/static/chunks/14jhy~xia8lh8.js +1 -0
- package/out/_next/static/chunks/{turbopack-0xta0kqwzkf28.js → turbopack-05qngmxam3ar~.js} +1 -1
- package/out/_not-found/__next._full.txt +12 -12
- package/out/_not-found/__next._head.txt +3 -3
- package/out/_not-found/__next._index.txt +7 -7
- 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 +2 -2
- package/out/_not-found/index.html +2 -2
- package/out/_not-found/index.txt +12 -12
- package/out/admin/__next._full.txt +14 -14
- package/out/admin/__next._head.txt +3 -3
- package/out/admin/__next._index.txt +7 -7
- package/out/admin/__next._tree.txt +3 -3
- package/out/admin/__next.admin.__PAGE__.txt +4 -4
- package/out/admin/__next.admin.txt +4 -4
- package/out/admin/index.html +2 -2
- package/out/admin/index.txt +14 -14
- package/out/app/__next._full.txt +13 -13
- package/out/app/__next._head.txt +3 -3
- package/out/app/__next._index.txt +7 -7
- package/out/app/__next._tree.txt +2 -2
- 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 +13 -13
- package/out/chat/__next._full.txt +14 -14
- package/out/chat/__next._head.txt +3 -3
- package/out/chat/__next._index.txt +7 -7
- package/out/chat/__next._tree.txt +3 -3
- package/out/chat/__next.chat.__PAGE__.txt +4 -4
- package/out/chat/__next.chat.txt +4 -4
- package/out/chat/index.html +2 -2
- package/out/chat/index.txt +14 -14
- package/out/chat/join/__next._full.txt +14 -14
- package/out/chat/join/__next._head.txt +3 -3
- package/out/chat/join/__next._index.txt +7 -7
- package/out/chat/join/__next._tree.txt +3 -3
- 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 +4 -4
- package/out/chat/join/index.html +2 -2
- package/out/chat/join/index.txt +14 -14
- package/out/download/__next._full.txt +34 -36
- package/out/download/__next._head.txt +3 -3
- package/out/download/__next._index.txt +7 -7
- package/out/download/__next._tree.txt +4 -4
- package/out/download/__next.download.__PAGE__.txt +7 -7
- package/out/download/__next.download.txt +3 -3
- package/out/download/index.html +2 -2
- package/out/download/index.txt +34 -36
- 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 +15 -15
- package/out/logo-512.png +0 -0
- package/out/logo.ico +0 -0
- package/out/logo.svg +12 -0
- package/out/note/__next._full.txt +13 -13
- package/out/note/__next._head.txt +3 -3
- package/out/note/__next._index.txt +7 -7
- package/out/note/__next._tree.txt +2 -2
- 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 +13 -13
- package/out/ping/__next._full.txt +14 -14
- package/out/ping/__next._head.txt +3 -3
- package/out/ping/__next._index.txt +7 -7
- package/out/ping/__next._tree.txt +3 -3
- package/out/ping/__next.ping.__PAGE__.txt +5 -5
- package/out/ping/__next.ping.txt +3 -3
- package/out/ping/index.html +2 -2
- package/out/ping/index.txt +14 -14
- package/out/web3/__next._full.txt +13 -13
- package/out/web3/__next._head.txt +3 -3
- package/out/web3/__next._index.txt +7 -7
- package/out/web3/__next._tree.txt +2 -2
- package/out/web3/__next.web3.__PAGE__.txt +4 -4
- package/out/web3/__next.web3.txt +3 -3
- package/out/web3/ed25519/__next._full.txt +11 -11
- package/out/web3/ed25519/__next._head.txt +3 -3
- package/out/web3/ed25519/__next._index.txt +7 -7
- package/out/web3/ed25519/__next._tree.txt +2 -2
- 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 +11 -11
- package/out/web3/index.html +2 -2
- package/out/web3/index.txt +13 -13
- package/out/web3/tools/__next._full.txt +11 -11
- package/out/web3/tools/__next._head.txt +3 -3
- package/out/web3/tools/__next._index.txt +7 -7
- package/out/web3/tools/__next._tree.txt +2 -2
- 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 +11 -11
- package/package.json +6 -5
- package/public/favicon.ico +0 -0
- package/public/logo-512.png +0 -0
- package/public/logo.ico +0 -0
- package/public/logo.svg +12 -0
- package/server/index.js +0 -8
- 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 +354 -413
- package/server/src/http/app.js +22 -14
- package/server/src/http/uploads.js +1 -0
- package/server/src/index.js +275 -90
- package/server/src/utils/avatar.js +3 -2
- package/out/_next/static/chunks/04mo7rr..0_1q.js +0 -1
- package/out/_next/static/chunks/06rf3qq5ggs6v.js +0 -1
- package/out/_next/static/chunks/07td.jq7xff84.css +0 -1
- package/out/_next/static/chunks/0_0oph_z1az14.js +0 -1
- package/out/_next/static/chunks/0ao1lbi4b.sfa.js +0 -1
- package/out/_next/static/chunks/0cl7d~7abnk_p.css +0 -1
- package/out/_next/static/chunks/0d306t1wvjpdx.js +0 -1
- package/out/_next/static/chunks/0g_a~e050bgzg.css +0 -1
- package/out/_next/static/chunks/0m_5nb6x8qy._.js +0 -1
- package/out/_next/static/chunks/0n.ayxmsar6e5.js +0 -1
- package/out/_next/static/chunks/0olqjomda37-e.js +0 -1
- package/out/_next/static/chunks/0qgx9t4jx16ua.css +0 -1
- package/out/_next/static/chunks/0s~g.l~x049o2.js +0 -1
- package/out/_next/static/chunks/0voe1.ttrh84k.css +0 -1
- package/out/_next/static/chunks/0wtf0xsiicxx6.js +0 -1
- package/out/_next/static/chunks/0x.ky97owcxxs.js +0 -1
- package/out/_next/static/chunks/0ysj5b94vu4ri.js +0 -1
- package/out/_next/static/chunks/153-sz7s.qml2.js +0 -1
- package/out/_next/static/chunks/17cwkb2yn_akx.js +0 -1
- package/out/_next/static/chunks/184hxsuf-5c84.js +0 -1
- package/out/gandengyan/__next._full.txt +0 -25
- package/out/gandengyan/__next._head.txt +0 -5
- package/out/gandengyan/__next._index.txt +0 -9
- package/out/gandengyan/__next._tree.txt +0 -5
- package/out/gandengyan/__next.gandengyan.__PAGE__.txt +0 -10
- package/out/gandengyan/__next.gandengyan.txt +0 -5
- package/out/gandengyan/index.html +0 -15
- package/out/gandengyan/index.txt +0 -25
- /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → 2smv1H9Y4Z2Ri-SL-UFgR}/_buildManifest.js +0 -0
- /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → 2smv1H9Y4Z2Ri-SL-UFgR}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{aPEZ4zaaR5W3WpSZ0dFsa → 2smv1H9Y4Z2Ri-SL-UFgR}/_ssgManifest.js +0 -0
- /package/out/{pwa-512x512.png → avatar.png} +0 -0
- /package/public/{pwa-512x512.png → avatar.png} +0 -0
|
@@ -1,173 +1,40 @@
|
|
|
1
|
-
import crypto from 'node:crypto'
|
|
2
|
-
|
|
3
1
|
const SUITS = ['S', 'H', 'C', 'D']
|
|
4
2
|
const RANKS = ['3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2']
|
|
5
3
|
const STRAIGHT_RANKS = RANKS.filter(rank => rank !== '2')
|
|
6
4
|
const RANK_VALUE = new Map(RANKS.map((rank, index) => [rank, index + 3]))
|
|
7
5
|
const INITIAL_HAND_SIZE = 5
|
|
8
6
|
const SEALED_PENALTY = 15
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
}
|
|
7
|
+
const INITIAL_SCORE = 1000
|
|
8
|
+
|
|
9
|
+
export function createGanDengYanRoom({
|
|
10
|
+
roomCode,
|
|
11
|
+
ownerAddress,
|
|
12
|
+
ownerName,
|
|
13
|
+
players = [],
|
|
14
|
+
random = Math.random,
|
|
15
|
+
}) {
|
|
16
|
+
const roomPlayers = normalizePlayers(players)
|
|
17
|
+
if (!roomPlayers.some(player => player.address === normalizeAddress(ownerAddress))) {
|
|
18
|
+
roomPlayers.unshift({
|
|
19
|
+
address: normalizeAddress(ownerAddress),
|
|
20
|
+
name: cleanName(ownerName),
|
|
21
|
+
})
|
|
121
22
|
}
|
|
122
23
|
|
|
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
24
|
const room = {
|
|
154
|
-
id:
|
|
155
|
-
|
|
25
|
+
id: String(roomCode || '').toUpperCase(),
|
|
26
|
+
ownerAddress: normalizeAddress(ownerAddress),
|
|
156
27
|
status: 'lobby',
|
|
157
|
-
|
|
158
|
-
players:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
score: 0,
|
|
168
|
-
playedCards: 0,
|
|
169
|
-
},
|
|
170
|
-
],
|
|
28
|
+
seq: 1,
|
|
29
|
+
players: roomPlayers.slice(0, 6).map((player, seat) => ({
|
|
30
|
+
address: player.address,
|
|
31
|
+
name: cleanName(player.name),
|
|
32
|
+
seat,
|
|
33
|
+
hand: [],
|
|
34
|
+
handCount: 0,
|
|
35
|
+
score: INITIAL_SCORE,
|
|
36
|
+
playedCards: 0,
|
|
37
|
+
})),
|
|
171
38
|
deck: [],
|
|
172
39
|
discard: [],
|
|
173
40
|
table: null,
|
|
@@ -181,185 +48,230 @@ function createRoom({ roomId, ownerId, ownerName, ownerAddress }) {
|
|
|
181
48
|
roundResult: null,
|
|
182
49
|
log: ['房间已创建'],
|
|
183
50
|
winnerSeat: null,
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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)
|
|
51
|
+
random,
|
|
52
|
+
}
|
|
53
|
+
return publicGanDengYanRoom(room)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function syncGanDengYanLobby(room, players = []) {
|
|
57
|
+
const state = hydrateGanDengYanRoom(room)
|
|
58
|
+
if (!state || state.status !== 'lobby') return state
|
|
59
|
+
const currentScores = new Map(state.players.map(player => [player.address, player.score]))
|
|
60
|
+
state.players = normalizePlayers(players)
|
|
61
|
+
.slice(0, 6)
|
|
62
|
+
.map((player, seat) => ({
|
|
63
|
+
address: player.address,
|
|
64
|
+
name: cleanName(player.name),
|
|
65
|
+
seat,
|
|
66
|
+
hand: [],
|
|
67
|
+
handCount: 0,
|
|
68
|
+
score: currentScores.get(player.address) ?? INITIAL_SCORE,
|
|
69
|
+
playedCards: 0,
|
|
70
|
+
}))
|
|
71
|
+
state.seq += 1
|
|
72
|
+
return publicGanDengYanRoom(state)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function startGanDengYanRound(room, random = Math.random) {
|
|
76
|
+
const state = hydrateGanDengYanRoom(room)
|
|
77
|
+
if (!state || state.players.length < 2) {
|
|
78
|
+
throw new Error('至少需要 2 名玩家')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
state.deck = shuffle(createDeck(), random)
|
|
82
|
+
state.discard = []
|
|
83
|
+
state.table = null
|
|
84
|
+
state.passSeats = []
|
|
85
|
+
state.winnerSeat = null
|
|
86
|
+
state.roundResult = null
|
|
87
|
+
state.baseScore = 1
|
|
88
|
+
state.bombCount = 0
|
|
89
|
+
state.status = 'playing'
|
|
90
|
+
for (const player of orderedPlayers(state)) {
|
|
91
|
+
player.hand = draw(state, INITIAL_HAND_SIZE)
|
|
92
|
+
player.handCount = player.hand.length
|
|
257
93
|
player.playedCards = 0
|
|
258
94
|
sortHand(player.hand)
|
|
259
95
|
}
|
|
260
|
-
const starter = chooseStarter(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
|
|
96
|
+
const starter = chooseStarter(state, random)
|
|
97
|
+
state.currentSeat = starter.seat
|
|
98
|
+
state.lastWinnerSeat = null
|
|
99
|
+
state.log = [`新一局开始,${starter.name} 先出牌`]
|
|
100
|
+
if (state.diceRolls.length > 0) {
|
|
101
|
+
state.log.unshift(
|
|
102
|
+
`骰子结果:${state.diceRolls
|
|
103
|
+
.map(roll => `${roll.name} ${roll.value}`)
|
|
104
|
+
.join(',')}`
|
|
105
|
+
)
|
|
266
106
|
}
|
|
267
|
-
|
|
107
|
+
state.seq += 1
|
|
108
|
+
return publicGanDengYanRoom(state)
|
|
268
109
|
}
|
|
269
110
|
|
|
270
|
-
function
|
|
271
|
-
const
|
|
272
|
-
|
|
111
|
+
export function playGanDengYanCards(room, address, cardIds) {
|
|
112
|
+
const state = hydrateGanDengYanRoom(room)
|
|
113
|
+
const player = currentPlayer(state)
|
|
114
|
+
const normalizedAddress = normalizeAddress(address)
|
|
115
|
+
if (!player || player.address !== normalizedAddress) {
|
|
116
|
+
return { ok: false, error: '还没轮到你', state: publicGanDengYanRoom(state) }
|
|
117
|
+
}
|
|
118
|
+
|
|
273
119
|
const cards = cardIds.map(id => player.hand.find(card => card.id === id))
|
|
274
|
-
if (cards.length === 0 || cards.some(card => !card))
|
|
120
|
+
if (cards.length === 0 || cards.some(card => !card)) {
|
|
121
|
+
return { ok: false, error: '手牌不存在', state: publicGanDengYanRoom(state) }
|
|
122
|
+
}
|
|
275
123
|
const combo = analyzeCards(cards)
|
|
276
|
-
if (!combo)
|
|
277
|
-
|
|
124
|
+
if (!combo) {
|
|
125
|
+
return { ok: false, error: '这个牌型不合法', state: publicGanDengYanRoom(state) }
|
|
126
|
+
}
|
|
127
|
+
if (!canBeat(combo, state.table?.combo)) {
|
|
128
|
+
return { ok: false, error: '出的牌压不过上一手', state: publicGanDengYanRoom(state) }
|
|
129
|
+
}
|
|
130
|
+
|
|
278
131
|
player.hand = player.hand.filter(card => !cardIds.includes(card.id))
|
|
132
|
+
player.handCount = player.hand.length
|
|
279
133
|
player.playedCards += cards.length
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
134
|
+
state.discard.push(...cards)
|
|
135
|
+
state.table = { seat: player.seat, playerName: player.name, cards, combo }
|
|
136
|
+
state.passSeats = []
|
|
137
|
+
state.lastWinnerSeat = player.seat
|
|
284
138
|
if (combo.type === 'bomb') {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
139
|
+
state.bombCount += 1
|
|
140
|
+
state.baseScore *= 2
|
|
141
|
+
}
|
|
142
|
+
state.log.unshift(
|
|
143
|
+
`${player.name} 出 ${combo.label} ${cards.map(labelCard).join(' ')}${
|
|
144
|
+
combo.type === 'bomb' ? `,底分 ${state.baseScore}` : ''
|
|
145
|
+
}`
|
|
146
|
+
)
|
|
289
147
|
if (player.hand.length === 0) {
|
|
290
|
-
finishGame(
|
|
148
|
+
finishGame(state, player)
|
|
291
149
|
} else {
|
|
292
|
-
advanceTurn(
|
|
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)
|
|
150
|
+
advanceTurn(state)
|
|
313
151
|
}
|
|
314
|
-
|
|
152
|
+
state.seq += 1
|
|
153
|
+
return { ok: true, state: publicGanDengYanRoom(state) }
|
|
315
154
|
}
|
|
316
155
|
|
|
317
|
-
function
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
if (
|
|
322
|
-
|
|
156
|
+
export function passGanDengYanTurn(room, address) {
|
|
157
|
+
const state = hydrateGanDengYanRoom(room)
|
|
158
|
+
const player = currentPlayer(state)
|
|
159
|
+
const normalizedAddress = normalizeAddress(address)
|
|
160
|
+
if (!player || player.address !== normalizedAddress) {
|
|
161
|
+
return { ok: false, error: '还没轮到你', state: publicGanDengYanRoom(state) }
|
|
162
|
+
}
|
|
163
|
+
if (!state.table) {
|
|
164
|
+
return { ok: false, error: '领出时不能不要', state: publicGanDengYanRoom(state) }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!state.passSeats.includes(player.seat)) state.passSeats.push(player.seat)
|
|
168
|
+
state.log.unshift(`${player.name} 不要`)
|
|
169
|
+
const activeSeats = activePlayers(state).map(item => item.seat)
|
|
170
|
+
const seatsToBeat = activeSeats.filter(seat => seat !== state.lastWinnerSeat)
|
|
171
|
+
if (seatsToBeat.every(seat => state.passSeats.includes(seat))) {
|
|
172
|
+
refillAfterRound(state)
|
|
173
|
+
state.currentSeat = state.lastWinnerSeat
|
|
174
|
+
state.table = null
|
|
175
|
+
state.passSeats = []
|
|
176
|
+
state.log.unshift('本轮结束,所有玩家各补 1 张,重新领出')
|
|
323
177
|
} else {
|
|
324
|
-
|
|
178
|
+
advanceTurn(state)
|
|
325
179
|
}
|
|
326
|
-
|
|
180
|
+
state.seq += 1
|
|
181
|
+
return { ok: true, state: publicGanDengYanRoom(state) }
|
|
327
182
|
}
|
|
328
183
|
|
|
329
|
-
function
|
|
184
|
+
export function publicGanDengYanRoom(room) {
|
|
185
|
+
if (!room) return null
|
|
330
186
|
return {
|
|
331
187
|
id: room.id,
|
|
332
|
-
|
|
188
|
+
ownerAddress: room.ownerAddress,
|
|
333
189
|
status: room.status,
|
|
334
|
-
|
|
335
|
-
deckCount: room.deck.
|
|
336
|
-
discardCount: room.discard.
|
|
337
|
-
currentSeat: room.currentSeat,
|
|
338
|
-
lastWinnerSeat:
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
190
|
+
seq: Number(room.seq || 1),
|
|
191
|
+
deckCount: room.deck?.length || Number(room.deckCount || 0),
|
|
192
|
+
discardCount: room.discard?.length || Number(room.discardCount || 0),
|
|
193
|
+
currentSeat: Number(room.currentSeat || 0),
|
|
194
|
+
lastWinnerSeat:
|
|
195
|
+
room.lastWinnerSeat === null || room.lastWinnerSeat === undefined
|
|
196
|
+
? null
|
|
197
|
+
: Number(room.lastWinnerSeat),
|
|
198
|
+
previousWinnerSeat:
|
|
199
|
+
room.previousWinnerSeat === null || room.previousWinnerSeat === undefined
|
|
200
|
+
? null
|
|
201
|
+
: Number(room.previousWinnerSeat),
|
|
202
|
+
baseScore: Number(room.baseScore || 1),
|
|
203
|
+
bombCount: Number(room.bombCount || 0),
|
|
204
|
+
diceRolls: Array.isArray(room.diceRolls) ? room.diceRolls : [],
|
|
205
|
+
roundResult: room.roundResult || null,
|
|
206
|
+
table: room.table
|
|
207
|
+
? { ...room.table, cards: room.table.cards.map(publicCard) }
|
|
208
|
+
: null,
|
|
209
|
+
passSeats: Array.isArray(room.passSeats) ? room.passSeats : [],
|
|
210
|
+
winnerSeat:
|
|
211
|
+
room.winnerSeat === null || room.winnerSeat === undefined
|
|
212
|
+
? null
|
|
213
|
+
: Number(room.winnerSeat),
|
|
214
|
+
log: Array.isArray(room.log) ? room.log.slice(0, 18) : [],
|
|
348
215
|
players: orderedPlayers(room).map(player => ({
|
|
349
|
-
id: player.id,
|
|
350
216
|
address: player.address,
|
|
351
217
|
name: player.name,
|
|
352
|
-
bot: player.bot,
|
|
353
218
|
seat: player.seat,
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
hand: player.id === viewerId ? player.hand.map(publicCard) : [],
|
|
219
|
+
handCount: player.hand?.length ?? player.handCount ?? 0,
|
|
220
|
+
score: Number(player.score ?? INITIAL_SCORE),
|
|
221
|
+
playedCards: Number(player.playedCards || 0),
|
|
222
|
+
hand: Array.isArray(player.hand) ? player.hand.map(publicCard) : [],
|
|
359
223
|
})),
|
|
360
224
|
}
|
|
361
225
|
}
|
|
362
226
|
|
|
227
|
+
export function hydrateGanDengYanRoom(input) {
|
|
228
|
+
if (!input || typeof input !== 'object') return null
|
|
229
|
+
return {
|
|
230
|
+
id: String(input.id || '').toUpperCase(),
|
|
231
|
+
ownerAddress: normalizeAddress(input.ownerAddress),
|
|
232
|
+
status:
|
|
233
|
+
input.status === 'playing' || input.status === 'finished'
|
|
234
|
+
? input.status
|
|
235
|
+
: 'lobby',
|
|
236
|
+
seq: Number(input.seq || 1),
|
|
237
|
+
players: Array.isArray(input.players)
|
|
238
|
+
? input.players.map(normalizeRoundPlayer).filter(Boolean)
|
|
239
|
+
: [],
|
|
240
|
+
deck: Array.isArray(input.deck) ? input.deck.map(normalizeCard).filter(Boolean) : [],
|
|
241
|
+
discard: Array.isArray(input.discard)
|
|
242
|
+
? input.discard.map(normalizeCard).filter(Boolean)
|
|
243
|
+
: [],
|
|
244
|
+
table:
|
|
245
|
+
input.table && typeof input.table === 'object'
|
|
246
|
+
? {
|
|
247
|
+
...input.table,
|
|
248
|
+
cards: Array.isArray(input.table.cards)
|
|
249
|
+
? input.table.cards.map(normalizeCard).filter(Boolean)
|
|
250
|
+
: [],
|
|
251
|
+
}
|
|
252
|
+
: null,
|
|
253
|
+
currentSeat: Number(input.currentSeat || 0),
|
|
254
|
+
lastWinnerSeat:
|
|
255
|
+
input.lastWinnerSeat === null || input.lastWinnerSeat === undefined
|
|
256
|
+
? null
|
|
257
|
+
: Number(input.lastWinnerSeat),
|
|
258
|
+
previousWinnerSeat:
|
|
259
|
+
input.previousWinnerSeat === null || input.previousWinnerSeat === undefined
|
|
260
|
+
? null
|
|
261
|
+
: Number(input.previousWinnerSeat),
|
|
262
|
+
passSeats: Array.isArray(input.passSeats) ? input.passSeats.map(Number) : [],
|
|
263
|
+
baseScore: Number(input.baseScore || 1),
|
|
264
|
+
bombCount: Number(input.bombCount || 0),
|
|
265
|
+
diceRolls: Array.isArray(input.diceRolls) ? input.diceRolls : [],
|
|
266
|
+
roundResult: input.roundResult || null,
|
|
267
|
+
log: Array.isArray(input.log) ? input.log.map(String) : [],
|
|
268
|
+
winnerSeat:
|
|
269
|
+
input.winnerSeat === null || input.winnerSeat === undefined
|
|
270
|
+
? null
|
|
271
|
+
: Number(input.winnerSeat),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
363
275
|
export function analyzeCards(cards) {
|
|
364
276
|
if (!cards?.length) return null
|
|
365
277
|
const jokerCount = cards.filter(isJoker).length
|
|
@@ -367,7 +279,9 @@ export function analyzeCards(cards) {
|
|
|
367
279
|
if (normals.length === 0) return null
|
|
368
280
|
const bomb = analyzeBomb(cards, normals, jokerCount)
|
|
369
281
|
if (bomb) return bomb
|
|
370
|
-
if (cards.length === 1 && jokerCount === 0)
|
|
282
|
+
if (cards.length === 1 && jokerCount === 0) {
|
|
283
|
+
return makeCombo('single', cardValue(cards[0]), 1, [cardValue(cards[0])])
|
|
284
|
+
}
|
|
371
285
|
if (cards.length === 2 && canRepresentSameRank(normals, jokerCount)) {
|
|
372
286
|
const value = sameRankValue(normals)
|
|
373
287
|
return makeCombo('pair', value, 2, [value, value])
|
|
@@ -378,14 +292,26 @@ export function analyzeCards(cards) {
|
|
|
378
292
|
function analyzeBomb(cards, normals, jokerCount) {
|
|
379
293
|
if (cards.length < 3 || !canRepresentSameRank(normals, jokerCount)) return null
|
|
380
294
|
const value = sameRankValue(normals)
|
|
381
|
-
return makeCombo(
|
|
295
|
+
return makeCombo(
|
|
296
|
+
'bomb',
|
|
297
|
+
value,
|
|
298
|
+
cards.length,
|
|
299
|
+
Array(cards.length).fill(value),
|
|
300
|
+
jokerCount === 0
|
|
301
|
+
)
|
|
382
302
|
}
|
|
383
303
|
|
|
384
304
|
function analyzeStraight(cards) {
|
|
385
305
|
if (cards.length < 3) return null
|
|
386
306
|
for (let start = 0; start <= STRAIGHT_RANKS.length - cards.length; start += 1) {
|
|
387
|
-
const values = STRAIGHT_RANKS.slice(start, start + cards.length).map(rank =>
|
|
388
|
-
|
|
307
|
+
const values = STRAIGHT_RANKS.slice(start, start + cards.length).map(rank =>
|
|
308
|
+
RANK_VALUE.get(rank)
|
|
309
|
+
)
|
|
310
|
+
if (
|
|
311
|
+
cards.every(
|
|
312
|
+
(card, index) => isJoker(card) || cardValue(card) === values[index]
|
|
313
|
+
)
|
|
314
|
+
) {
|
|
389
315
|
return makeCombo('straight', values.at(-1), cards.length, values)
|
|
390
316
|
}
|
|
391
317
|
}
|
|
@@ -396,9 +322,15 @@ function analyzePairStraight(cards) {
|
|
|
396
322
|
if (cards.length < 4 || cards.length % 2 !== 0) return null
|
|
397
323
|
const pairCount = cards.length / 2
|
|
398
324
|
for (let start = 0; start <= STRAIGHT_RANKS.length - pairCount; start += 1) {
|
|
399
|
-
const pairValues = STRAIGHT_RANKS.slice(start, start + pairCount).map(rank =>
|
|
325
|
+
const pairValues = STRAIGHT_RANKS.slice(start, start + pairCount).map(rank =>
|
|
326
|
+
RANK_VALUE.get(rank)
|
|
327
|
+
)
|
|
400
328
|
const values = pairValues.flatMap(value => [value, value])
|
|
401
|
-
if (
|
|
329
|
+
if (
|
|
330
|
+
cards.every(
|
|
331
|
+
(card, index) => isJoker(card) || cardValue(card) === values[index]
|
|
332
|
+
)
|
|
333
|
+
) {
|
|
402
334
|
return makeCombo('pairStraight', pairValues.at(-1), cards.length, values)
|
|
403
335
|
}
|
|
404
336
|
}
|
|
@@ -406,7 +338,14 @@ function analyzePairStraight(cards) {
|
|
|
406
338
|
}
|
|
407
339
|
|
|
408
340
|
function makeCombo(type, value, length, resolvedValues, pure = true) {
|
|
409
|
-
return {
|
|
341
|
+
return {
|
|
342
|
+
type,
|
|
343
|
+
value,
|
|
344
|
+
length,
|
|
345
|
+
resolvedValues,
|
|
346
|
+
pure,
|
|
347
|
+
label: labelCombo({ type, value, length, resolvedValues, pure }),
|
|
348
|
+
}
|
|
410
349
|
}
|
|
411
350
|
|
|
412
351
|
function canBeat(combo, tableCombo) {
|
|
@@ -421,39 +360,59 @@ function canBeat(combo, tableCombo) {
|
|
|
421
360
|
}
|
|
422
361
|
if (combo.length !== tableCombo.length) return false
|
|
423
362
|
if (combo.type === 'single' || combo.type === 'pair') {
|
|
424
|
-
return
|
|
363
|
+
return (
|
|
364
|
+
combo.value === nextValue(tableCombo.value) ||
|
|
365
|
+
(combo.value === RANK_VALUE.get('2') && tableCombo.value !== RANK_VALUE.get('2'))
|
|
366
|
+
)
|
|
425
367
|
}
|
|
426
368
|
return combo.value === nextValue(tableCombo.value)
|
|
427
369
|
}
|
|
428
370
|
|
|
429
|
-
function
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
371
|
+
function normalizePlayers(players) {
|
|
372
|
+
const seen = new Set()
|
|
373
|
+
return players
|
|
374
|
+
.map(player => ({
|
|
375
|
+
address: normalizeAddress(player.address),
|
|
376
|
+
name: cleanName(player.name),
|
|
377
|
+
publicKey: String(player.publicKey || ''),
|
|
378
|
+
}))
|
|
379
|
+
.filter(player => {
|
|
380
|
+
if (!player.address || seen.has(player.address)) return false
|
|
381
|
+
seen.add(player.address)
|
|
382
|
+
return true
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function normalizeRoundPlayer(input) {
|
|
387
|
+
const address = normalizeAddress(input.address)
|
|
388
|
+
if (!address) return null
|
|
389
|
+
return {
|
|
390
|
+
address,
|
|
391
|
+
name: cleanName(input.name),
|
|
392
|
+
seat: Number(input.seat || 0),
|
|
393
|
+
hand: Array.isArray(input.hand) ? input.hand.map(normalizeCard).filter(Boolean) : [],
|
|
394
|
+
handCount: Number(input.handCount || 0),
|
|
395
|
+
score: Number(input.score ?? INITIAL_SCORE),
|
|
396
|
+
playedCards: Number(input.playedCards || 0),
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function normalizeCard(card) {
|
|
401
|
+
if (!card || typeof card !== 'object') return null
|
|
402
|
+
const rank = String(card.rank || '')
|
|
403
|
+
const suit = String(card.suit || '')
|
|
404
|
+
if (!RANK_VALUE.has(rank) && rank !== 'SJ' && rank !== 'BJ') return null
|
|
405
|
+
if (!SUITS.includes(suit) && suit !== 'Joker') return null
|
|
443
406
|
return {
|
|
444
|
-
id:
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
connected: true,
|
|
450
|
-
hand: [],
|
|
451
|
-
score: 0,
|
|
452
|
-
playedCards: 0,
|
|
407
|
+
id: String(card.id || `${suit}-${rank}`),
|
|
408
|
+
suit,
|
|
409
|
+
rank,
|
|
410
|
+
label: card.label || labelCard({ suit, rank }),
|
|
411
|
+
color: card.color || cardColor({ suit, rank }),
|
|
453
412
|
}
|
|
454
413
|
}
|
|
455
414
|
|
|
456
|
-
function chooseStarter(room) {
|
|
415
|
+
function chooseStarter(room, random) {
|
|
457
416
|
if (room.previousWinnerSeat !== null) {
|
|
458
417
|
room.diceRolls = []
|
|
459
418
|
return orderedPlayers(room).find(player => player.seat === room.previousWinnerSeat) || orderedPlayers(room)[0]
|
|
@@ -461,14 +420,20 @@ function chooseStarter(room) {
|
|
|
461
420
|
let candidates = orderedPlayers(room)
|
|
462
421
|
let rolls = []
|
|
463
422
|
while (candidates.length > 1) {
|
|
464
|
-
rolls = candidates.map(player => ({
|
|
423
|
+
rolls = candidates.map(player => ({
|
|
424
|
+
seat: player.seat,
|
|
425
|
+
name: player.name,
|
|
426
|
+
value: rollDice(random),
|
|
427
|
+
}))
|
|
465
428
|
const max = Math.max(...rolls.map(roll => roll.value))
|
|
466
429
|
const winners = rolls.filter(roll => roll.value === max)
|
|
467
430
|
if (winners.length === 1) {
|
|
468
431
|
room.diceRolls = rolls
|
|
469
432
|
return candidates.find(player => player.seat === winners[0].seat)
|
|
470
433
|
}
|
|
471
|
-
candidates = candidates.filter(player =>
|
|
434
|
+
candidates = candidates.filter(player =>
|
|
435
|
+
winners.some(roll => roll.seat === player.seat)
|
|
436
|
+
)
|
|
472
437
|
}
|
|
473
438
|
room.diceRolls = rolls
|
|
474
439
|
return candidates[0]
|
|
@@ -486,10 +451,23 @@ function finishGame(room, winner) {
|
|
|
486
451
|
const loss = sealed ? SEALED_PENALTY : player.hand.length * room.baseScore
|
|
487
452
|
player.score -= loss
|
|
488
453
|
winnerGain += loss
|
|
489
|
-
losers.push({
|
|
454
|
+
losers.push({
|
|
455
|
+
seat: player.seat,
|
|
456
|
+
name: player.name,
|
|
457
|
+
loss,
|
|
458
|
+
sealed,
|
|
459
|
+
cardsLeft: player.hand.length,
|
|
460
|
+
})
|
|
490
461
|
}
|
|
491
462
|
winner.score += winnerGain
|
|
492
|
-
room.roundResult = {
|
|
463
|
+
room.roundResult = {
|
|
464
|
+
winnerSeat: winner.seat,
|
|
465
|
+
winnerName: winner.name,
|
|
466
|
+
winnerGain,
|
|
467
|
+
baseScore: room.baseScore,
|
|
468
|
+
bombCount: room.bombCount,
|
|
469
|
+
losers,
|
|
470
|
+
}
|
|
493
471
|
room.log.unshift(`${winner.name} 获胜,赢 ${winnerGain} 分`)
|
|
494
472
|
}
|
|
495
473
|
|
|
@@ -497,34 +475,27 @@ function refillAfterRound(room) {
|
|
|
497
475
|
for (const player of activePlayers(room)) {
|
|
498
476
|
if (room.deck.length === 0) break
|
|
499
477
|
player.hand.push(...draw(room, 1))
|
|
478
|
+
player.handCount = player.hand.length
|
|
500
479
|
sortHand(player.hand)
|
|
501
480
|
}
|
|
502
481
|
}
|
|
503
482
|
|
|
504
|
-
function
|
|
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) {
|
|
483
|
+
function createDeck() {
|
|
513
484
|
const cards = []
|
|
514
|
-
for (
|
|
515
|
-
for (const
|
|
516
|
-
|
|
485
|
+
for (const suit of SUITS) {
|
|
486
|
+
for (const rank of RANKS) {
|
|
487
|
+
cards.push({ id: `${suit}-${rank}`, suit, rank })
|
|
517
488
|
}
|
|
518
|
-
cards.push({ id: `${deck}-SJ`, suit: 'Joker', rank: 'SJ', deck })
|
|
519
|
-
cards.push({ id: `${deck}-BJ`, suit: 'Joker', rank: 'BJ', deck })
|
|
520
489
|
}
|
|
521
|
-
|
|
490
|
+
cards.push({ id: 'SJ', suit: 'Joker', rank: 'SJ' })
|
|
491
|
+
cards.push({ id: 'BJ', suit: 'Joker', rank: 'BJ' })
|
|
492
|
+
return cards.map(publicCard)
|
|
522
493
|
}
|
|
523
494
|
|
|
524
|
-
function shuffle(cards) {
|
|
495
|
+
function shuffle(cards, random) {
|
|
525
496
|
const copy = [...cards]
|
|
526
497
|
for (let index = copy.length - 1; index > 0; index -= 1) {
|
|
527
|
-
const swapIndex = Math.floor(
|
|
498
|
+
const swapIndex = Math.floor(random() * (index + 1))
|
|
528
499
|
;[copy[index], copy[swapIndex]] = [copy[swapIndex], copy[index]]
|
|
529
500
|
}
|
|
530
501
|
return copy
|
|
@@ -547,7 +518,7 @@ function activePlayers(room) {
|
|
|
547
518
|
}
|
|
548
519
|
|
|
549
520
|
function orderedPlayers(room) {
|
|
550
|
-
return [...room
|
|
521
|
+
return [...(room?.players || [])].sort((a, b) => a.seat - b.seat)
|
|
551
522
|
}
|
|
552
523
|
|
|
553
524
|
function advanceTurn(room) {
|
|
@@ -556,40 +527,7 @@ function advanceTurn(room) {
|
|
|
556
527
|
room.currentSeat = seats[(currentIndex + 1) % seats.length]
|
|
557
528
|
}
|
|
558
529
|
|
|
559
|
-
function
|
|
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) {
|
|
530
|
+
function canRepresentSameRank(normals, jokerCount = 0) {
|
|
593
531
|
if (normals.length === 0) return false
|
|
594
532
|
const first = normals[0].rank
|
|
595
533
|
return normals.every(card => card.rank === first) && normals.length + jokerCount >= 2
|
|
@@ -616,7 +554,13 @@ function suitValue(suit) {
|
|
|
616
554
|
}
|
|
617
555
|
|
|
618
556
|
function publicCard(card) {
|
|
619
|
-
return {
|
|
557
|
+
return {
|
|
558
|
+
id: card.id,
|
|
559
|
+
suit: card.suit,
|
|
560
|
+
rank: card.rank,
|
|
561
|
+
label: labelCard(card),
|
|
562
|
+
color: cardColor(card),
|
|
563
|
+
}
|
|
620
564
|
}
|
|
621
565
|
|
|
622
566
|
function labelCard(card) {
|
|
@@ -630,11 +574,15 @@ function suitSymbol(suit) {
|
|
|
630
574
|
}
|
|
631
575
|
|
|
632
576
|
function cardColor(card) {
|
|
633
|
-
return card.suit === 'H' || card.suit === 'D' || card.rank === 'BJ'
|
|
577
|
+
return card.suit === 'H' || card.suit === 'D' || card.rank === 'BJ'
|
|
578
|
+
? 'red'
|
|
579
|
+
: 'black'
|
|
634
580
|
}
|
|
635
581
|
|
|
636
582
|
function labelCombo(combo) {
|
|
637
|
-
const resolved = combo.resolvedValues?.length
|
|
583
|
+
const resolved = combo.resolvedValues?.length
|
|
584
|
+
? `(${combo.resolvedValues.map(valueLabel).join(' ')})`
|
|
585
|
+
: ''
|
|
638
586
|
return `${{
|
|
639
587
|
single: '单张',
|
|
640
588
|
pair: '对子',
|
|
@@ -654,22 +602,15 @@ function nextValue(value) {
|
|
|
654
602
|
return RANK_VALUE.get(RANKS[index + 1])
|
|
655
603
|
}
|
|
656
604
|
|
|
657
|
-
function
|
|
658
|
-
|
|
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) || '玩家'
|
|
605
|
+
function rollDice(random) {
|
|
606
|
+
return Math.floor(random() * 6) + 1
|
|
667
607
|
}
|
|
668
608
|
|
|
669
|
-
function
|
|
670
|
-
|
|
609
|
+
function normalizeAddress(value) {
|
|
610
|
+
const address = String(value || '').trim()
|
|
611
|
+
return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''
|
|
671
612
|
}
|
|
672
613
|
|
|
673
|
-
function
|
|
674
|
-
return
|
|
614
|
+
function cleanName(name) {
|
|
615
|
+
return String(name || '玩家').trim().slice(0, 16) || '玩家'
|
|
675
616
|
}
|