631 lines
19 KiB
JavaScript
631 lines
19 KiB
JavaScript
/**
|
|
* 斗地主UI控制器
|
|
* 连接游戏逻辑和界面
|
|
*/
|
|
|
|
let game = null;
|
|
let userScore = 0; // 当前用户积分
|
|
|
|
// ========== 用户积分相关 ==========
|
|
const GAME_ID = 'doudizhu';
|
|
const API_BASE = ''; // 同域,相对路径
|
|
|
|
// 获取用户积分
|
|
async function loadUserScore() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/user/score/${GAME_ID}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
// 未登录,显示提示
|
|
document.getElementById('userName').textContent = '未登录';
|
|
document.getElementById('scoreValue').textContent = '-';
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (data.score !== undefined) {
|
|
userScore = data.score;
|
|
document.getElementById('scoreValue').textContent = userScore;
|
|
}
|
|
} catch (error) {
|
|
console.error('获取积分失败:', error);
|
|
document.getElementById('scoreValue').textContent = '-';
|
|
}
|
|
}
|
|
|
|
// 获取用户名
|
|
async function loadUserInfo() {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/auth/status`, {
|
|
credentials: 'include'
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.authenticated && data.user) {
|
|
document.getElementById('userName').textContent = data.user.username;
|
|
} else {
|
|
document.getElementById('userName').textContent = '游客';
|
|
}
|
|
} catch (error) {
|
|
document.getElementById('userName').textContent = '游客';
|
|
}
|
|
}
|
|
|
|
// 提交游戏积分
|
|
async function submitScore(points) {
|
|
if (points <= 0) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/api/user/score`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
game: GAME_ID,
|
|
points: points
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
userScore = data.score;
|
|
document.getElementById('scoreValue').textContent = userScore;
|
|
|
|
// 显示获得积分动画
|
|
showScoreAnimation(points);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('提交积分失败:', error);
|
|
}
|
|
}
|
|
|
|
// 显示获得积分动画
|
|
function showScoreAnimation(points) {
|
|
const scoreEl = document.getElementById('scoreValue');
|
|
const anim = document.createElement('div');
|
|
anim.textContent = `+${points}`;
|
|
anim.style.cssText = `
|
|
position: fixed;
|
|
top: 40px;
|
|
right: 20px;
|
|
color: #ffd700;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
text-shadow: 0 0 20px rgba(255, 215, 0, 0.8);
|
|
pointer-events: none;
|
|
z-index: 1001;
|
|
animation: scoreFloat 1.5s ease-out forwards;
|
|
`;
|
|
document.body.appendChild(anim);
|
|
|
|
// 高亮分数
|
|
scoreEl.style.transform = 'scale(1.3)';
|
|
scoreEl.style.transition = 'transform 0.3s';
|
|
setTimeout(() => {
|
|
scoreEl.style.transform = 'scale(1)';
|
|
}, 300);
|
|
|
|
setTimeout(() => anim.remove(), 1500);
|
|
}
|
|
|
|
// 添加分数浮动动画
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
@keyframes scoreFloat {
|
|
0% { opacity: 1; transform: translateY(0) scale(1); }
|
|
100% { opacity: 0; transform: translateY(-30px) scale(1.2); }
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
// ========== 牌的HTML渲染 ==========
|
|
function createCardHTML(card, selectable = true) {
|
|
if (!card) return '';
|
|
|
|
if (card.isJoker) {
|
|
const color = card.rank === '大王' ? '#e74c3c' : '#3498db';
|
|
return `
|
|
<div class="card joker" data-id="${card.id}" ${selectable ? 'onclick="toggleSelect(this)"' : ''}>
|
|
<div class="card-top" style="color:${color};font-size:12px;">${card.rank}</div>
|
|
<div class="card-center" style="font-size:32px;color:${color};">🃏</div>
|
|
<div class="card-bottom" style="color:${color};font-size:12px;transform:rotate(180deg);">${card.rank}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
const isRed = card.suit === '♥' || card.suit === '♦';
|
|
const color = isRed ? '#e74c3c' : '#2c3e50';
|
|
|
|
return `
|
|
<div class="card" data-id="${card.id}" data-rank="${card.rank}" data-suit="${card.suit}" ${selectable ? 'onclick="toggleSelect(this)"' : ''}>
|
|
<div class="card-top" style="color:${color};font-size:14px;line-height:1.2;">${card.rank}<br>${card.suit}</div>
|
|
<div class="card-center" style="color:${color};font-size:40px;">${card.suit}</div>
|
|
<div class="card-bottom" style="color:${color};font-size:14px;line-height:1.2;transform:rotate(180deg);">${card.rank}<br>${card.suit}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function createCardBackHTML() {
|
|
return '<div class="card back"></div>';
|
|
}
|
|
|
|
// ========== 选牌交互 ==========
|
|
function toggleSelect(cardEl) {
|
|
if (game && game.currentPlayer !== 0) return; // 不是玩家回合不能选
|
|
cardEl.classList.toggle('selected');
|
|
updateActionButtons();
|
|
}
|
|
|
|
function updateActionButtons() {
|
|
const selected = document.querySelectorAll('.hand-cards .card.selected');
|
|
const hasSelection = selected.length > 0;
|
|
const isPlayerTurn = game && game.currentPlayer === 0 && game.gamePhase === 'playing';
|
|
|
|
document.getElementById('btn-play').disabled = !hasSelection || !isPlayerTurn;
|
|
document.getElementById('btn-pass').disabled = !isPlayerTurn;
|
|
document.getElementById('btn-hint').disabled = !isPlayerTurn;
|
|
}
|
|
|
|
// ========== 游戏流程控制 ==========
|
|
function startGame() {
|
|
document.getElementById('start-screen').style.display = 'none';
|
|
|
|
game = new DoudizhuGame();
|
|
game.init();
|
|
|
|
// 发牌动画
|
|
document.getElementById('game-status').textContent = '正在发牌...';
|
|
animateDeal();
|
|
}
|
|
|
|
function animateDeal() {
|
|
const handContainer = document.getElementById('hand-cards');
|
|
handContainer.innerHTML = '';
|
|
handContainer.classList.add('dealing');
|
|
|
|
// 先快速发完(动画效果)
|
|
let cardIndex = 0;
|
|
const dealInterval = setInterval(() => {
|
|
const playerIndex = cardIndex % 3;
|
|
|
|
if (playerIndex === 0) {
|
|
// 玩家显示背面
|
|
const cardEl = document.createElement('div');
|
|
cardEl.innerHTML = createCardBackHTML();
|
|
handContainer.appendChild(cardEl.firstElementChild);
|
|
}
|
|
|
|
updateAICardCounts();
|
|
cardIndex++;
|
|
|
|
if (cardIndex >= 51) {
|
|
clearInterval(dealInterval);
|
|
handContainer.classList.remove('dealing');
|
|
|
|
// 真正发牌
|
|
const result = game.deal();
|
|
|
|
// 显示底牌
|
|
showBottomCardsBack();
|
|
|
|
// 显示玩家手牌
|
|
renderPlayerCards();
|
|
|
|
// 进入叫地主
|
|
setTimeout(() => {
|
|
showCallLordModal();
|
|
}, 500);
|
|
}
|
|
}, 30);
|
|
}
|
|
|
|
function renderPlayerCards() {
|
|
const container = document.getElementById('hand-cards');
|
|
container.innerHTML = '';
|
|
|
|
const playerCards = game.players[0].cards;
|
|
playerCards.forEach(card => {
|
|
const div = document.createElement('div');
|
|
div.innerHTML = createCardHTML(card, true);
|
|
container.appendChild(div.firstElementChild);
|
|
});
|
|
}
|
|
|
|
function updateAICardCounts() {
|
|
document.querySelector('.left-player .card-count').textContent =
|
|
game ? game.players[1].cards.length + '张' : '17张';
|
|
document.querySelector('.right-player .card-count').textContent =
|
|
game ? game.players[2].cards.length + '张' : '17张';
|
|
}
|
|
|
|
function showBottomCardsBack() {
|
|
const deckArea = document.querySelector('.deck-cards');
|
|
deckArea.innerHTML = '';
|
|
for (let i = 0; i < 3; i++) {
|
|
deckArea.innerHTML += createCardBackHTML();
|
|
}
|
|
}
|
|
|
|
function showBottomCardsFront(cards) {
|
|
const deckArea = document.querySelector('.deck-cards');
|
|
deckArea.innerHTML = '';
|
|
cards.forEach(card => {
|
|
const div = document.createElement('div');
|
|
div.innerHTML = createCardHTML(card, false);
|
|
deckArea.appendChild(div.firstElementChild);
|
|
});
|
|
}
|
|
|
|
// ========== 叫地主 ==========
|
|
function showCallLordModal() {
|
|
document.getElementById('call-lord-modal').style.display = 'flex';
|
|
document.getElementById('game-status').textContent = '轮到你了:是否叫地主?';
|
|
}
|
|
|
|
function callLord(want) {
|
|
document.getElementById('call-lord-modal').style.display = 'none';
|
|
|
|
const result = game.callLord(0, want);
|
|
|
|
if (result.success) {
|
|
becomeLord(0);
|
|
} else {
|
|
// AI叫地主
|
|
aiCallLordProcess();
|
|
}
|
|
}
|
|
|
|
function becomeLord(playerId) {
|
|
const player = game.players[playerId];
|
|
|
|
// 显示底牌
|
|
showBottomCardsFront(game.bottomCards);
|
|
|
|
// 更新角色显示
|
|
updateRoleDisplay();
|
|
|
|
// 如果是玩家,更新手牌
|
|
if (playerId === 0) {
|
|
renderPlayerCards();
|
|
document.getElementById('game-status').textContent = '你是地主,请出牌';
|
|
updateActionButtons();
|
|
} else {
|
|
document.getElementById('game-status').textContent = player.name + '是地主,游戏开始';
|
|
setTimeout(() => aiTurn(), 1500);
|
|
}
|
|
}
|
|
|
|
function updateRoleDisplay() {
|
|
game.players.forEach(p => {
|
|
const roleEl = document.getElementById(p.id === 0 ? 'my-role' :
|
|
(p.id === 1 ? 'left-role' : 'right-role'));
|
|
if (roleEl) {
|
|
roleEl.textContent = p.isLord ? '地主' : '农民';
|
|
}
|
|
});
|
|
}
|
|
|
|
function aiCallLordProcess() {
|
|
document.getElementById('game-status').textContent = '等待其他玩家...';
|
|
|
|
setTimeout(() => {
|
|
// AI1叫地主
|
|
if (game.players[1].ai.wantToBeLord(game.players[1].cards)) {
|
|
game.callLord(1, true);
|
|
becomeLord(1);
|
|
return;
|
|
}
|
|
|
|
// AI2叫地主
|
|
setTimeout(() => {
|
|
if (game.players[2].ai.wantToBeLord(game.players[2].cards)) {
|
|
game.callLord(2, true);
|
|
becomeLord(2);
|
|
return;
|
|
}
|
|
|
|
// 都没叫,玩家默认当地主
|
|
game.callLord(0, true);
|
|
becomeLord(0);
|
|
}, 1000);
|
|
}, 1000);
|
|
}
|
|
|
|
// ========== 出牌逻辑 ==========
|
|
function playCards() {
|
|
const selected = document.querySelectorAll('.hand-cards .card.selected');
|
|
if (selected.length === 0) return;
|
|
|
|
const cardIds = Array.from(selected).map(el => parseInt(el.dataset.id));
|
|
const cards = game.players[0].cards.filter(c => cardIds.includes(c.id));
|
|
|
|
const result = game.playCards(0, cards);
|
|
|
|
if (!result.success) {
|
|
showToast(result.error);
|
|
return;
|
|
}
|
|
|
|
// 更新UI
|
|
executePlayUI(0, cards, result.cardType);
|
|
|
|
if (result.gameOver) {
|
|
endGame(0);
|
|
return;
|
|
}
|
|
|
|
// AI回合
|
|
setTimeout(() => aiTurn(), 1000);
|
|
}
|
|
|
|
function passTurn() {
|
|
// 先取消所有选中的牌(让牌回归原位)
|
|
document.querySelectorAll('.hand-cards .card.selected').forEach(card => {
|
|
card.classList.remove('selected');
|
|
});
|
|
updateActionButtons();
|
|
|
|
const result = game.pass(0);
|
|
|
|
showPlayedCards([], 0, '不出');
|
|
|
|
if (result.clearDesktop) {
|
|
clearDesktop();
|
|
}
|
|
|
|
setTimeout(() => aiTurn(), 1000);
|
|
}
|
|
|
|
function executePlayUI(playerId, cards, cardType) {
|
|
// 从手牌移除
|
|
if (playerId === 0) {
|
|
renderPlayerCards();
|
|
}
|
|
|
|
// 显示到桌面
|
|
const typeName = cardType ? cardType.name : '出牌';
|
|
showPlayedCards(cards, playerId, typeName);
|
|
|
|
// 更新AI牌数
|
|
updateAICardCounts();
|
|
|
|
// 取消选中
|
|
updateActionButtons();
|
|
}
|
|
|
|
function showPlayedCards(cards, playerId, actionName) {
|
|
const container = document.querySelector('.played-cards');
|
|
const playerName = game.players[playerId].name;
|
|
const playerRole = game.players[playerId].isLord ? '【地主】' : '【农民】';
|
|
|
|
// 如果是"不出",只更新文字,不清空牌(保留上一轮出的牌)
|
|
if (cards.length === 0) {
|
|
document.querySelector('.played-by').textContent = `${playerName}${playerRole}: 不出`;
|
|
document.getElementById('game-status').textContent = `${playerName}选择不出`;
|
|
return;
|
|
}
|
|
|
|
// 有牌出,更新牌显示
|
|
container.innerHTML = '';
|
|
container.classList.add('playing');
|
|
|
|
cards.forEach(card => {
|
|
const div = document.createElement('div');
|
|
div.innerHTML = createCardHTML(card, false);
|
|
container.appendChild(div.firstElementChild);
|
|
});
|
|
|
|
// 显示出牌者 + 牌型
|
|
document.querySelector('.played-by').textContent = `${playerName}${playerRole}: ${actionName}`;
|
|
document.getElementById('game-status').textContent = `${playerName}出了 ${actionName}`;
|
|
|
|
setTimeout(() => container.classList.remove('playing'), 300);
|
|
}
|
|
|
|
function clearDesktop() {
|
|
setTimeout(() => {
|
|
document.querySelector('.played-cards').innerHTML = '';
|
|
document.querySelector('.played-by').textContent = '新一轮出牌';
|
|
game.desktopCards = null;
|
|
}, 1000);
|
|
}
|
|
|
|
// ========== AI回合 ==========
|
|
function aiTurn() {
|
|
if (!game || game.gamePhase !== 'playing') return;
|
|
|
|
const currentId = game.currentPlayer;
|
|
if (currentId === 0) {
|
|
document.getElementById('game-status').textContent = '轮到你了';
|
|
updateActionButtons();
|
|
return;
|
|
}
|
|
|
|
const player = game.players[currentId];
|
|
document.getElementById('game-status').textContent = player.name + '正在思考...';
|
|
|
|
setTimeout(() => {
|
|
const result = game.aiPlay();
|
|
|
|
if (result.success) {
|
|
if (result.cardType) {
|
|
// AI出了牌
|
|
executePlayUI(currentId, result.cardType.cards, result.cardType);
|
|
} else {
|
|
// AI选择不出
|
|
showPlayedCards([], currentId, '不出');
|
|
}
|
|
|
|
if (result.clearDesktop) {
|
|
clearDesktop();
|
|
}
|
|
|
|
if (result.gameOver) {
|
|
endGame(result.winner);
|
|
return;
|
|
}
|
|
|
|
// 继续下一个
|
|
setTimeout(() => aiTurn(), 1500);
|
|
}
|
|
}, 1500);
|
|
}
|
|
|
|
// ========== 提示功能 ==========
|
|
function showHint() {
|
|
if (!game || game.currentPlayer !== 0) return;
|
|
|
|
const cards = game.players[0].cards;
|
|
if (cards.length === 0) return;
|
|
|
|
// 取消之前的选中
|
|
document.querySelectorAll('.hand-cards .card.selected').forEach(c => c.classList.remove('selected'));
|
|
|
|
// 找出一个合法的出牌
|
|
const ai = new DoudizhuAI(0);
|
|
const validPlays = ai.findAllValidPlays(cards, game.desktopCards);
|
|
|
|
if (validPlays.length > 0) {
|
|
// 选最小的
|
|
validPlays.sort((a, b) => a.type.value - b.type.value);
|
|
const hintCards = validPlays[0].cards;
|
|
|
|
hintCards.forEach(card => {
|
|
const cardEl = document.querySelector(`.hand-cards .card[data-id="${card.id}"]`);
|
|
if (cardEl) cardEl.classList.add('selected');
|
|
});
|
|
|
|
updateActionButtons();
|
|
showToast('提示: ' + validPlays[0].type.name);
|
|
} else {
|
|
showToast('没有可出的牌,建议不出');
|
|
}
|
|
}
|
|
|
|
// ========== 游戏结束 ==========
|
|
function endGame(winnerId) {
|
|
const winner = game.players[winnerId];
|
|
const isPlayerWin = winnerId === 0;
|
|
const isLordWin = winner.isLord;
|
|
|
|
let message;
|
|
let earnedPoints = 0;
|
|
|
|
if (isPlayerWin) {
|
|
// 玩家获胜,计算积分
|
|
earnedPoints = isLordWin ? 3 : 1;
|
|
const roleName = isLordWin ? '地主' : '农民';
|
|
message = `恭喜你,${roleName}获胜!\n获得 ${earnedPoints} 积分`;
|
|
|
|
// 提交积分
|
|
submitScore(earnedPoints);
|
|
} else {
|
|
message = isLordWin ? `${winner.name}(地主)获胜!` : '农民获胜!\n你输了';
|
|
}
|
|
|
|
document.getElementById('game-status').textContent = message.replace('\n', ' ');
|
|
|
|
setTimeout(() => {
|
|
if (confirm(message + '\n\n是否重新开始?')) {
|
|
location.reload();
|
|
}
|
|
}, 800);
|
|
}
|
|
|
|
// ========== 工具函数 ==========
|
|
function showToast(message) {
|
|
// 简单的提示
|
|
const status = document.getElementById('game-status');
|
|
const oldText = status.textContent;
|
|
status.textContent = message;
|
|
setTimeout(() => {
|
|
status.textContent = oldText;
|
|
}, 2000);
|
|
}
|
|
|
|
// ========== 登录检查 ==========
|
|
async function checkLogin() {
|
|
try {
|
|
const response = await fetch('/api/auth/status', {
|
|
credentials: 'include'
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (!data.authenticated) {
|
|
// 未登录,显示遮罩
|
|
showLoginRequired();
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch (error) {
|
|
showLoginRequired();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function showLoginRequired() {
|
|
// 创建登录提示遮罩
|
|
const overlay = document.createElement('div');
|
|
overlay.id = 'login-required-overlay';
|
|
overlay.innerHTML = `
|
|
<div style="
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0,0,0,0.85);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10000;
|
|
backdrop-filter: blur(8px);
|
|
">
|
|
<div style="
|
|
background: linear-gradient(145deg, #1e293b 0%, #0f172a 100%);
|
|
padding: 40px;
|
|
border-radius: 16px;
|
|
text-align: center;
|
|
max-width: 320px;
|
|
border: 1px solid rgba(255, 215, 0, 0.3);
|
|
">
|
|
<div style="font-size: 48px; margin-bottom: 16px;">🔒</div>
|
|
<div style="font-size: 20px; font-weight: bold; color: #fff; margin-bottom: 8px;">请先登录</div>
|
|
<div style="font-size: 14px; color: #888; margin-bottom: 24px;">登录后才能游玩斗地主</div>
|
|
<button onclick="location.href='/'" style="
|
|
padding: 12px 32px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
">去登录</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(overlay);
|
|
}
|
|
|
|
// ========== 事件绑定 ==========
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
// 先检查登录
|
|
const isLoggedIn = await checkLogin();
|
|
if (!isLoggedIn) return;
|
|
|
|
// 加载用户信息和积分
|
|
loadUserInfo();
|
|
loadUserScore();
|
|
|
|
document.getElementById('btn-start').addEventListener('click', startGame);
|
|
document.getElementById('btn-call-yes').addEventListener('click', () => callLord(true));
|
|
document.getElementById('btn-call-no').addEventListener('click', () => callLord(false));
|
|
document.getElementById('btn-play').addEventListener('click', playCards);
|
|
document.getElementById('btn-pass').addEventListener('click', passTurn);
|
|
document.getElementById('btn-hint').addEventListener('click', showHint);
|
|
});
|