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) { document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden')); $(id).classList.remove('hidden'); } function chat(name, msg, sys) { const p = document.createElement('p'); p.innerHTML = sys ? '' + msg + '' : '' + name + ': ' + msg; $('chatMsgs').appendChild(p); $('chatMsgs').scrollTop = 1e6; } function authHeaders() { return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }; } async function getCaptcha(type) { try { const res = await fetch('/api/auth/captcha'); const d = await res.json(); if (d.code === 0) { if (type === 'login') { loginCaptchaId = d.data.captchaId; $('loginCaptchaImg').src = d.data.image; } else { regCaptchaId = d.data.captchaId; $('regCaptchaImg').src = d.data.image; } } } catch (e) { console.error('Failed to get captcha:', e); } } async function login() { const usernameVal = $('loginUsername').value.trim(); const passwordVal = $('loginPassword').value; const captchaVal = $('loginCaptcha').value.trim().toUpperCase(); if (!usernameVal || !passwordVal || !captchaVal) { alert('请填写完整信息'); return; } try { const res = await fetch('/api/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ username: usernameVal, password: passwordVal, captcha: captchaVal, captchaId: loginCaptchaId }) }); const d = await res.json(); if (d.code === 0) { token = d.data.token; userId = d.data.userId; username = d.data.username; nickname = d.data.nickname; localStorage.setItem('token', token); localStorage.setItem('userId', userId); localStorage.setItem('username', username); localStorage.setItem('nickname', nickname); showLobby(); } else { alert(d.message); getCaptcha('login'); } } catch (e) { alert('登录失败: ' + e.message); getCaptcha('login'); } } async function register() { const usernameVal = $('regUsername').value.trim(); const passwordVal = $('regPassword').value; const nicknameVal = $('regNickname').value.trim(); const captchaVal = $('regCaptcha').value.trim().toUpperCase(); if (!usernameVal || !passwordVal || !nicknameVal || !captchaVal) { alert('请填写完整信息'); return; } if (usernameVal.length < 3 || usernameVal.length > 20) { alert('用户名需要3-20个字符'); return; } if (passwordVal.length < 6) { alert('密码至少6位'); return; } try { const res = await fetch('/api/auth/register', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ username: usernameVal, password: passwordVal, nickname: nicknameVal, captcha: captchaVal, captchaId: regCaptchaId }) }); const d = await res.json(); if (d.code === 0) { token = d.data.token; userId = d.data.userId; username = d.data.username; nickname = d.data.nickname; localStorage.setItem('token', token); localStorage.setItem('userId', userId); localStorage.setItem('username', username); localStorage.setItem('nickname', nickname); showLobby(); } else { alert(d.message); getCaptcha('register'); } } catch (e) { alert('注册失败: ' + e.message); getCaptcha('register'); } } async function logout() { try { await fetch('/api/auth/logout', { method: 'POST', headers: authHeaders() }); } catch (e) {} token = ''; userId = ''; username = ''; nickname = ''; localStorage.removeItem('token'); localStorage.removeItem('userId'); localStorage.removeItem('username'); localStorage.removeItem('nickname'); show('auth'); getCaptcha('login'); } async function validateToken() { if (!token) return false; try { const res = await fetch('/api/auth/validate', { headers: authHeaders() }); const d = await res.json(); if (d.code === 0) { userId = d.data.userId; username = d.data.username; nickname = d.data.nickname; return true; } } catch (e) {} return false; } async function checkCurrentRoom() { try { const res = await fetch('/api/rooms/current', { headers: authHeaders() }); const d = await res.json(); if (d.code === 0 && d.data && d.data.roomId) { roomId = d.data.roomId; playerId = d.data.playerId; return true; } } catch (e) {} return false; } async function showLobby() { $('welcomeUser').textContent = '欢迎, ' + nickname; $('playerName').value = nickname; show('lobby'); } async function create() { const name = $('playerName').value.trim(); if (!name) { alert('请输入昵称'); return; } try { const res = await fetch('/api/rooms', { method: 'POST', headers: authHeaders(), body: JSON.stringify({playerName: name, maxPlayers: +$('maxPlayers').value}) }); const d = await res.json(); if (d.code === 0) { roomId = d.data.roomId; playerId = d.data.playerId; connect(); } else { alert(d.message); } } catch (e) { alert('创建失败: ' + e.message); } } async function join() { const name = $('playerName').value.trim(); const rid = $('roomIdInput').value.trim().toLowerCase(); if (!name) { alert('请输入昵称'); return; } if (!rid) { alert('请输入房间号'); return; } try { const res = await fetch('/api/rooms/' + rid + '/join', { method: 'POST', headers: authHeaders(), body: JSON.stringify({playerName: name}) }); const d = await res.json(); if (d.code === 0) { roomId = d.data.roomId; playerId = d.data.playerId; connect(); } else { alert(d.message); } } catch (e) { alert('加入失败: ' + e.message); } } async function leaveRoom() { try { await fetch('/api/rooms/leave', { method: 'POST', headers: authHeaders() }); } catch (e) {} if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } if (ws) ws.close(); show('lobby'); roomId = ''; playerId = ''; state = null; selected = []; } 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); 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); else if (msg.type === 'gameOver') showGameOver(msg.data); else if (msg.type === 'chat') chat(msg.data.playerName, msg.data.message); else if (msg.type === 'leave') chat('', '有玩家离开房间', true); else if (msg.type === 'error') chat('', msg.data, true); }; ws.onerror = 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})); } } function render(s) { state = s; $('displayRoomId').textContent = s.id; $('roundNum').textContent = s.roundCount; var me = null; for (var i = 0; i < s.players.length; i++) { if (s.players[i].id === playerId) { me = s.players[i]; break; } } if (s.state === 0) { show('waiting'); var html = ''; for (var i = 0; i < s.players.length; i++) { var p = s.players[i]; html += '
'; html += '
' + p.name + (p.id === playerId ? ' (你)' : '') + '
'; html += '
' + (p.isReady ? '已准备' : '等待中') + '
'; } $('playerList').innerHTML = html; $('readyBtn').textContent = (me && me.isReady) ? '取消准备' : '准备'; } else if (s.state === 1) { show('game'); var cur = s.players[s.currentTurn]; $('turnInfo').textContent = cur.id === playerId ? '轮到你出牌' : '等待 ' + cur.name + ' 出牌'; var othersHtml = ''; for (var i = 0; i < s.players.length; i++) { var p = s.players[i]; if (p.id === playerId) continue; othersHtml += '
'; othersHtml += '
' + p.name + '
'; othersHtml += '
' + p.cardCount + '张
'; } $('others').innerHTML = othersHtml; if (s.lastPlay) { $('lastPlay').classList.remove('hidden'); var lpName = ''; for (var i = 0; i < s.players.length; i++) { if (s.players[i].id === s.lastPlay.playerId) { lpName = s.players[i].name; break; } } $('lastPlayer').textContent = lpName; $('lastCards').innerHTML = s.lastPlay.cards.map(function(c) { return makeCardHtml(c, true); }).join(''); } else { $('lastPlay').classList.add('hidden'); } renderMyCards(me); updateBtns(); } else if (s.state === 2) { show('waiting'); var html = ''; for (var i = 0; i < s.players.length; i++) { var p = s.players[i]; html += '
'; html += '
' + p.name + '
'; html += '
' + (p.isReady ? '已准备' : '等待中') + '
'; } $('playerList').innerHTML = html; $('readyBtn').textContent = (me && me.isReady) ? '取消准备' : '准备'; } } function getCardClass(c) { if (c.suit === 5) return 'super'; if (c.suit === 4) return 'joker'; return c.suit < 2 ? 'red' : 'black'; } function getCardText(c) { if (c.suit === 5) return '超人强'; if (c.suit === 4) return c.value === 16 ? '小王' : '大王'; var suits = ['♥', '♦', '♣', '♠']; var vals = ['', '', '', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A', '2']; return suits[c.suit] + vals[c.value]; } function makeCardHtml(c, small) { return '
' + getCardText(c) + '
'; } function renderMyCards(me) { var el = $('myCards'); el.innerHTML = ''; if (!me || !me.cards) return; for (var i = 0; i < me.cards.length; i++) { (function(c) { var key = c.suit + '_' + c.value; var div = document.createElement('div'); div.className = 'card ' + getCardClass(c) + (selected.indexOf(key) >= 0 ? ' selected' : ''); div.textContent = getCardText(c); div.onclick = function() { var idx = selected.indexOf(key); if (idx === -1) { selected.push(key); div.classList.add('selected'); } else { selected.splice(idx, 1); div.classList.remove('selected'); } updateBtns(); }; el.appendChild(div); })(me.cards[i]); } } function getValueCounts(cards) { var counts = {}; for (var i = 0; i < cards.length; i++) { var v = cards[i].value; counts[v] = (counts[v] || 0) + 1; } 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; }); 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 === 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 (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 seqPairs = true; for (var i = 1; i < pairVals.length; i++) { 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) { // 先比张数,张数多的赢;张数相同再比牌值 var myCounts = getValueCounts(cards); var lastCounts = getValueCounts(lastPlay.cards); var myCount = 0, lastCount = 0; for (var k in myCounts) { if (myCounts[k] >= 3) myCount = myCounts[k]; } for (var k in lastCounts) { if (lastCounts[k] >= 3) lastCount = lastCounts[k]; } if (myCount !== lastCount) return myCount > lastCount; var myMain = 0, lastMain = 0; for (var k in myCounts) { if (myCounts[k] >= 3 && parseInt(k) > myMain) myMain = parseInt(k); } for (var k in lastCounts) { if (lastCounts[k] >= 3 && parseInt(k) > lastMain) lastMain = parseInt(k); } return myMain > lastMain; } return lastPlay.cardType !== 9; } 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); var myMaxC = 0, lastMaxC = 0; for (var k in myCounts) { if (myCounts[k] > myMaxC || (myCounts[k] === myMaxC && parseInt(k) > myMain)) { myMaxC = myCounts[k]; myMain = parseInt(k); } } for (var k in lastCounts) { if (lastCounts[k] > lastMaxC || (lastCounts[k] === lastMaxC && parseInt(k) > lastMain)) { lastMaxC = lastCounts[k]; lastMain = parseInt(k); } } return myMain > lastMain; } function updateBtns() { var myTurn = state && state.players[state.currentTurn].id === playerId; var cards = getSelectedCards(); var validPlay = selected.length > 0 && canPlay(cards, state ? state.lastPlay : null); $('playBtn').disabled = !myTurn || !validPlay; var canPass = state && state.lastPlay && state.lastPlay.playerId !== playerId; $('passBtn').disabled = !myTurn || !canPass; } function getSelectedCards() { if (!state) return []; var me = null; for (var i = 0; i < state.players.length; i++) { if (state.players[i].id === playerId) { me = state.players[i]; break; } } if (!me || !me.cards) return []; return me.cards.filter(function(c) { return selected.indexOf(c.suit + '_' + c.value) >= 0; }); } function play() { if (selected.length === 0) return; var cards = getSelectedCards(); if (!canPlay(cards, state ? state.lastPlay : null)) { chat('', '牌型无效或管不上', true); return; } send('play', {cards: cards}); selected = []; } function pass() { send('pass', {}); } function toggleReady() { var me = null; if (state) { for (var i = 0; i < state.players.length; i++) { if (state.players[i].id === playerId) { me = state.players[i]; break; } } } send('ready', {ready: me ? !me.isReady : true}); } function leave() { leaveRoom(); } function showGameOver(d) { var w = null; if (state) { for (var i = 0; i < state.players.length; i++) { if (state.players[i].id === d.winnerId) { w = state.players[i]; break; } } } setTimeout(function() { $('winnerText').textContent = w ? (w.id === playerId ? '你赢了!' : w.name + ' 获胜') : '游戏结束'; $('gameOver').classList.remove('hidden'); }, 1000); } function again() { $('gameOver').classList.add('hidden'); send('ready', {ready: false}); } document.addEventListener('DOMContentLoaded', async function() { $('loginTab').onclick = function() { $('loginTab').classList.add('active'); $('registerTab').classList.remove('active'); $('loginForm').classList.remove('hidden'); $('registerForm').classList.add('hidden'); getCaptcha('login'); }; $('registerTab').onclick = function() { $('registerTab').classList.add('active'); $('loginTab').classList.remove('active'); $('registerForm').classList.remove('hidden'); $('loginForm').classList.add('hidden'); getCaptcha('register'); }; $('loginCaptchaImg').onclick = function() { getCaptcha('login'); }; $('regCaptchaImg').onclick = function() { getCaptcha('register'); }; $('loginBtn').onclick = login; $('registerBtn').onclick = register; $('logoutBtn').onclick = logout; $('createBtn').onclick = create; $('joinBtn').onclick = join; $('readyBtn').onclick = toggleReady; $('leaveBtn').onclick = leaveRoom; $('playBtn').onclick = play; $('passBtn').onclick = pass; $('againBtn').onclick = again; $('chatBtn').onclick = function() { var v = $('chatInput').value.trim(); if (v) { send('chat', v); $('chatInput').value = ''; } }; $('chatInput').onkeypress = function(e) { if (e.key === 'Enter') $('chatBtn').click(); }; $('roomIdInput').onkeypress = function(e) { if (e.key === 'Enter') join(); }; token = localStorage.getItem('token') || ''; userId = localStorage.getItem('userId') || ''; username = localStorage.getItem('username') || ''; nickname = localStorage.getItem('nickname') || ''; if (token) { var valid = await validateToken(); if (valid) { var inRoom = await checkCurrentRoom(); if (inRoom) { connect(); return; } showLobby(); return; } localStorage.removeItem('token'); localStorage.removeItem('userId'); localStorage.removeItem('username'); localStorage.removeItem('nickname'); } show('auth'); getCaptcha('login'); });