Files
nanocode/happy-games/games/doudizhu/ui.js
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

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);
});