fix: 修复牌型识别、获胜显示、出牌区布局、移动端重连

1. 牌型识别: 新增三带一对、飞机、飞机带单/带对、四带二、四带两对
2. 获胜显示: 先显示最后出牌1秒后再弹出获胜对话框
3. 出牌区布局: 横向排列并换行
4. 移动端重连: 切回前台时自动重连WebSocket
5. 更新文档: README.md和API.md补充新增牌型
This commit is contained in:
wtz
2026-02-22 10:00:13 +08:00
parent af3b805dbf
commit 97e03acbe2
7 changed files with 279 additions and 74 deletions

View File

@@ -45,6 +45,7 @@ h3{color:#ffd700;margin-bottom:10px}
.play-area{min-height:100px;background:rgba(0,0,0,.2);border-radius:10px;display:flex;align-items:center;justify-content:center;margin-bottom:15px}
.last-play{text-align:center}
.last-play span{color:#ffd700;font-size:14px}
#lastCards{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-top:8px}
.my-area{background:rgba(0,0,0,.3);border-radius:10px;padding:15px}
.my-cards{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;margin-bottom:12px;min-height:80px}
.card{width:50px;height:70px;background:linear-gradient(135deg,#fff,#f0f0f0);border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;cursor:pointer;transition:.15s;user-select:none}
@@ -63,6 +64,6 @@ h3{color:#ffd700;margin-bottom:10px}
.chat-input{display:flex;padding:8px;gap:6px;background:rgba(0,0,0,.3)}
.chat-input input{flex:1;padding:6px;border:none;border-radius:4px;background:rgba(255,255,255,.1);color:#fff;font-size:13px}
.modal{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.8);display:flex;align-items:center;justify-content:center}
.modal-box{background:#16213e;padding:30px;border-radius:12px;text-align:center;border:2px solid #ffd700}
.modal-box{background:#16213e;padding:30px;border-radius:12px;text-align:center;border:2px solid #ffd700;max-width:400px}
.modal-box h2{margin-bottom:20px}
@media(max-width:600px){#app{padding:10px}.card{width:42px;height:60px;font-size:12px}.chat{width:100%;bottom:0;right:0;border-radius:8px 8px 0 0}}

View File

@@ -1,6 +1,8 @@
let ws = null, playerId = '', roomId = '', state = null, selected = [];
let token = '', userId = '', username = '', nickname = '';
let loginCaptchaId = '', regCaptchaId = '';
let reconnectTimer = null;
let reconnectDelay = 1000;
const $ = id => document.getElementById(id);
function show(id) {
@@ -246,6 +248,10 @@ async function leaveRoom() {
headers: authHeaders()
});
} catch (e) {}
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
if (ws) ws.close();
show('lobby');
roomId = '';
@@ -257,7 +263,14 @@ async function leaveRoom() {
function connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(proto + '//' + location.host + '/api/ws?roomId=' + roomId + '&playerId=' + playerId);
ws.onopen = function() { chat('', '已连接', true); };
ws.onopen = function() {
chat('', '已连接', true);
reconnectDelay = 1000;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
};
ws.onmessage = function(e) {
const msg = JSON.parse(e.data);
if (msg.type === 'state') render(msg.data);
@@ -267,9 +280,41 @@ function connect() {
else if (msg.type === 'error') chat('', msg.data, true);
};
ws.onerror = function() { chat('', '连接错误', true); };
ws.onclose = function() { chat('', '断开连接', true); };
ws.onclose = function() {
chat('', '断开连接', true);
scheduleReconnect();
};
}
function scheduleReconnect() {
if (reconnectTimer) return;
if (!roomId || !playerId) return;
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
reconnectTimer = setTimeout(function() {
reconnectTimer = null;
if (roomId && playerId) {
chat('', '正在重连...', true);
connect();
}
}, reconnectDelay);
}
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
if (ws) {
try { ws.close(); } catch (e) {}
ws = null;
}
} else {
if (!ws || ws.readyState !== 1) {
if (roomId && playerId) {
reconnectDelay = 500;
scheduleReconnect();
}
}
}
});
function send(type, data) {
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({type: type, playerId: playerId, roomId: roomId, data: data}));
@@ -386,49 +431,86 @@ function getValueCounts(cards) {
return counts;
}
function isConsecutive(values) {
if (values.length < 2) return true;
values = values.slice().sort(function(a, b) { return a - b; });
for (var i = 1; i < values.length; i++) {
if (values[i] !== values[i - 1] + 1 || values[i] >= 15) return false;
}
return values[0] < 15;
}
function getCardType(cards) {
var n = cards.length;
if (n === 0) return 0;
var isRocket = n === 2 && cards.some(function(c) { return c.suit === 4 && c.value === 16; })
&& cards.some(function(c) { return c.suit === 4 && c.value === 17; });
var isRocket = n === 2 && cards.some(function(c) { return c.suit === 4 && c.value === 16; })
&& cards.some(function(c) { return c.suit === 4 && c.value === 17; });
if (isRocket) return 9;
var counts = getValueCounts(cards);
var keys = Object.keys(counts);
var singles = [], pairs = [], triples = [], quads = [];
for (var v in counts) {
var c = counts[v];
var val = parseInt(v);
if (c === 1) singles.push(val);
else if (c === 2) pairs.push(val);
else if (c === 3) triples.push(val);
else if (c === 4) quads.push(val);
}
if (keys.length === 1 && n >= 3) return 8;
if (quads.length === 1 && singles.length === 2 && pairs.length === 0 && triples.length === 0 && n === 6) return 13;
if (quads.length === 1 && pairs.length === 2 && singles.length === 0 && triples.length === 0 && n === 8) return 14;
if (triples.length >= 2 && isConsecutive(triples)) {
var planeCount = triples.length;
if (n === planeCount * 3) return 10;
if (n === planeCount * 3 + planeCount && singles.length === planeCount) return 11;
if (n === planeCount * 3 + planeCount * 2 && pairs.length === planeCount) return 12;
}
if (n === 1) return 1;
if (n === 2 && keys.length === 1) return 2;
if (n === 3 && keys.length === 1) return 8;
if (n === 4 && keys.length === 1) return 8;
if (n === 4 && keys.length === 2) {
for (var k in counts) { if (counts[k] === 3) return 4; }
}
if (n === 4 && triples.length === 1 && singles.length === 1) return 4;
if (n === 5 && triples.length === 1 && pairs.length === 1) return 5;
var sorted = cards.slice().sort(function(a, b) { return a.value - b.value; });
var isSeq = true;
for (var i = 1; i < sorted.length; i++) {
if (sorted[i].value !== sorted[i-1].value + 1 || sorted[i].value >= 15) { isSeq = false; break; }
if (sorted[i].value !== sorted[i - 1].value + 1 || sorted[i].value >= 15) { isSeq = false; break; }
}
if (n >= 3 && keys.length === n && isSeq) return 6;
var allPairs = true;
for (var k in counts) { if (counts[k] !== 2 || parseInt(k) >= 15) allPairs = false; }
var pairVals = keys.map(function(k) { return parseInt(k); }).sort(function(a,b){return a-b;});
var pairVals = keys.map(function(k) { return parseInt(k); }).sort(function(a, b) { return a - b; });
var seqPairs = true;
for (var i = 1; i < pairVals.length; i++) {
if (pairVals[i] !== pairVals[i-1] + 1) seqPairs = false;
if (pairVals[i] !== pairVals[i - 1] + 1) seqPairs = false;
}
if (n >= 4 && n % 2 === 0 && allPairs && seqPairs) return 7;
return 0;
}
function getTripleValues(cards) {
var counts = getValueCounts(cards);
var triples = [];
for (var v in counts) {
if (counts[v] >= 3) triples.push(parseInt(v));
}
return triples;
}
function canPlay(cards, lastPlay) {
var cardType = getCardType(cards);
if (cardType === 0) return false;
if (!lastPlay) return true;
if (cardType === 9) return true;
if (cardType === 8) {
if (lastPlay.cardType === 8) {
@@ -443,7 +525,14 @@ function canPlay(cards, lastPlay) {
}
if (lastPlay.cardType === 8 || lastPlay.cardType === 9) return false;
if (cardType !== lastPlay.cardType || cards.length !== lastPlay.cards.length) return false;
if (cardType === 10 || cardType === 11 || cardType === 12) {
var myTriples = getTripleValues(cards).sort(function(a, b) { return a - b; });
var lastTriples = getTripleValues(lastPlay.cards).sort(function(a, b) { return a - b; });
if (myTriples.length !== lastTriples.length) return false;
return myTriples[myTriples.length - 1] > lastTriples[lastTriples.length - 1];
}
var myMain = 0, lastMain = 0;
var myCounts = getValueCounts(cards);
var lastCounts = getValueCounts(lastPlay.cards);
@@ -514,8 +603,10 @@ function showGameOver(d) {
if (state.players[i].id === d.winnerId) { w = state.players[i]; break; }
}
}
$('winnerText').textContent = w ? (w.id === playerId ? '你赢了!' : w.name + ' 获胜') : '游戏结束';
$('gameOver').classList.remove('hidden');
setTimeout(function() {
$('winnerText').textContent = w ? (w.id === playerId ? '你赢了!' : w.name + ' 获胜') : '游戏结束';
$('gameOver').classList.remove('hidden');
}, 1000);
}
function again() {