// 2048游戏核心逻辑 class Game2048 { constructor() { this.size = 4; this.grid = []; this.score = 0; this.gameWon = false; this.gameOver = false; this.moved = false; // 游戏统计数据 this.stats = { moves: 0, startTime: null, gameTime: 0, maxTile: 2, mergeCount: 0 }; this.initializeGrid(); this.updateDisplay(); this.addRandomTile(); this.addRandomTile(); this.updateDisplay(); // 绑定事件 this.bindEvents(); // 开始计时 this.startTimer(); } // 依据分数计算权重(0.1 ~ 0.95) calculateWeightByScore(score) { const w = score / 4000; // 4000分约接近满权重 return Math.max(0.1, Math.min(0.95, w)); } // 按权重偏向生成0~10的随机整数,权重越高越偏向更大值 biasedRandomInt(maxInclusive, weight) { const rand = Math.random(); const biased = Math.pow(rand, 1 - weight); // weight越大,biased越接近1 const val = Math.floor(biased * (maxInclusive + 1)); return Math.max(0, Math.min(maxInclusive, val)); } // 附加结束信息到界面 appendEndInfo(text, type = 'info') { const message = document.getElementById('game-message'); if (!message) return; const info = document.createElement('div'); info.style.marginTop = '10px'; info.style.fontSize = '16px'; info.style.color = type === 'error' ? '#d9534f' : (type === 'success' ? '#28a745' : '#776e65'); info.textContent = text; message.appendChild(info); } // 游戏结束时尝试给当前登录账户加“萌芽币” async tryAwardCoinsOnGameOver() { try { const token = localStorage.getItem('token'); if (!token) { this.appendEndInfo('未登录,无法获得萌芽币'); return; } let email = null; try { const userStr = localStorage.getItem('user'); if (userStr) { const userObj = JSON.parse(userStr); email = userObj && (userObj.email || userObj['邮箱']); } } catch (e) { // 忽略解析错误 } if (!email) { this.appendEndInfo('未找到账户信息(email),无法加币', 'error'); return; } // 根据分数计算权重与概率 const weight = this.calculateWeightByScore(this.score); let awardProbability = weight; // 默认用权重作为概率 let guaranteed = false; // 分数≥500时必定触发奖励 if (this.score >= 500) { awardProbability = 1; guaranteed = true; } const roll = Math.random(); if (roll > awardProbability) { this.appendEndInfo('本局未获得萌芽币'); return; } // 生成0~10随机萌芽币数量,权重越高越偏向更大值 let coins = this.biasedRandomInt(5, weight); // 保底至少 1 个(仅当分数≥500时) if (guaranteed) { coins = Math.max(1, coins); } coins = Math.max(0, Math.min(10, coins)); if (coins <= 0) { this.appendEndInfo('本局未获得萌芽币'); return; } // 后端 API base URL(从父窗口ENV_CONFIG获取,回退到本地默认) const apiBase = (window.parent && window.parent.ENV_CONFIG && window.parent.ENV_CONFIG.API_URL) ? window.parent.ENV_CONFIG.API_URL : ((window.ENV_CONFIG && window.ENV_CONFIG.API_URL) ? window.ENV_CONFIG.API_URL : 'http://127.0.0.1:5002'); const resp = await fetch(`${apiBase}/api/user/add-coins`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ email, amount: coins }) }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); const msg = err && (err.message || err.error) ? (err.message || err.error) : `请求失败(${resp.status})`; this.appendEndInfo(`加币失败:${msg}`, 'error'); return; } const data = await resp.json(); if (data && data.success) { const newCoins = data.data && data.data.new_coins; this.appendEndInfo(`恭喜获得 ${coins} 个萌芽币!当前余额:${newCoins}`, 'success'); } else { const msg = (data && (data.message || data.error)) || '未知错误'; this.appendEndInfo(`加币失败:${msg}`, 'error'); } } catch (e) { console.error('加币流程发生错误:', e); this.appendEndInfo('加币失败:网络或系统错误', 'error'); } } initializeGrid() { this.grid = []; for (let i = 0; i < this.size; i++) { this.grid[i] = []; for (let j = 0; j < this.size; j++) { this.grid[i][j] = 0; } } } addRandomTile() { const emptyCells = []; for (let i = 0; i < this.size; i++) { for (let j = 0; j < this.size; j++) { if (this.grid[i][j] === 0) { emptyCells.push({x: i, y: j}); } } } if (emptyCells.length > 0) { const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)]; const value = Math.random() < 0.9 ? 2 : 4; this.grid[randomCell.x][randomCell.y] = value; // 创建新方块动画 this.createTileElement(randomCell.x, randomCell.y, value, true); } } createTileElement(x, y, value, isNew = false) { const container = document.getElementById('tile-container'); const tile = document.createElement('div'); tile.className = `tile tile-${value}`; if (isNew) tile.classList.add('tile-new'); tile.textContent = value; tile.style.left = `${y * (100/4)}%`; tile.style.top = `${x * (100/4)}%`; tile.dataset.x = x; tile.dataset.y = y; tile.dataset.value = value; container.appendChild(tile); // 移除动画类 setTimeout(() => { tile.classList.remove('tile-new'); }, 200); } updateDisplay() { // 清除所有方块 const container = document.getElementById('tile-container'); container.innerHTML = ''; // 重新创建所有方块 for (let i = 0; i < this.size; i++) { for (let j = 0; j < this.size; j++) { if (this.grid[i][j] !== 0) { this.createTileElement(i, j, this.grid[i][j]); } } } // 更新分数 document.getElementById('score').textContent = this.score; // 更新统计数据显示 if (window.gameStats) { window.gameStats.updateDisplay(); } } move(direction) { if (this.gameOver) return; this.moved = false; const previousGrid = this.grid.map(row => [...row]); switch (direction) { case 'up': this.moveUp(); break; case 'down': this.moveDown(); break; case 'left': this.moveLeft(); break; case 'right': this.moveRight(); break; } if (this.moved) { this.stats.moves++; this.addRandomTile(); this.updateDisplay(); if (this.isGameWon() && !this.gameWon) { this.gameWon = true; this.showGameWon(); } else if (this.isGameOver()) { this.gameOver = true; this.showGameOver(); } } } moveLeft() { for (let i = 0; i < this.size; i++) { const row = this.grid[i].filter(val => val !== 0); const merged = []; for (let j = 0; j < row.length - 1; j++) { if (row[j] === row[j + 1] && !merged[j] && !merged[j + 1]) { row[j] *= 2; this.score += row[j]; this.stats.mergeCount++; this.stats.maxTile = Math.max(this.stats.maxTile, row[j]); row[j + 1] = 0; merged[j] = true; } } const newRow = row.filter(val => val !== 0); while (newRow.length < this.size) { newRow.push(0); } for (let j = 0; j < this.size; j++) { if (this.grid[i][j] !== newRow[j]) { this.moved = true; } this.grid[i][j] = newRow[j]; } } } moveRight() { for (let i = 0; i < this.size; i++) { const row = this.grid[i].filter(val => val !== 0); const merged = []; for (let j = row.length - 1; j > 0; j--) { if (row[j] === row[j - 1] && !merged[j] && !merged[j - 1]) { row[j] *= 2; this.score += row[j]; this.stats.mergeCount++; this.stats.maxTile = Math.max(this.stats.maxTile, row[j]); row[j - 1] = 0; merged[j] = true; } } const newRow = row.filter(val => val !== 0); while (newRow.length < this.size) { newRow.unshift(0); } for (let j = 0; j < this.size; j++) { if (this.grid[i][j] !== newRow[j]) { this.moved = true; } this.grid[i][j] = newRow[j]; } } } moveUp() { for (let j = 0; j < this.size; j++) { const column = []; for (let i = 0; i < this.size; i++) { if (this.grid[i][j] !== 0) { column.push(this.grid[i][j]); } } const merged = []; for (let i = 0; i < column.length - 1; i++) { if (column[i] === column[i + 1] && !merged[i] && !merged[i + 1]) { column[i] *= 2; this.score += column[i]; this.stats.mergeCount++; this.stats.maxTile = Math.max(this.stats.maxTile, column[i]); column[i + 1] = 0; merged[i] = true; } } const newColumn = column.filter(val => val !== 0); while (newColumn.length < this.size) { newColumn.push(0); } for (let i = 0; i < this.size; i++) { if (this.grid[i][j] !== newColumn[i]) { this.moved = true; } this.grid[i][j] = newColumn[i]; } } } moveDown() { for (let j = 0; j < this.size; j++) { const column = []; for (let i = 0; i < this.size; i++) { if (this.grid[i][j] !== 0) { column.push(this.grid[i][j]); } } const merged = []; for (let i = column.length - 1; i > 0; i--) { if (column[i] === column[i - 1] && !merged[i] && !merged[i - 1]) { column[i] *= 2; this.score += column[i]; this.stats.mergeCount++; this.stats.maxTile = Math.max(this.stats.maxTile, column[i]); column[i - 1] = 0; merged[i] = true; } } const newColumn = column.filter(val => val !== 0); while (newColumn.length < this.size) { newColumn.unshift(0); } for (let i = 0; i < this.size; i++) { if (this.grid[i][j] !== newColumn[i]) { this.moved = true; } this.grid[i][j] = newColumn[i]; } } } isGameWon() { for (let i = 0; i < this.size; i++) { for (let j = 0; j < this.size; j++) { if (this.grid[i][j] === 2048) { return true; } } } return false; } isGameOver() { // 检查是否有空格 for (let i = 0; i < this.size; i++) { for (let j = 0; j < this.size; j++) { if (this.grid[i][j] === 0) { return false; } } } // 检查是否可以合并 for (let i = 0; i < this.size; i++) { for (let j = 0; j < this.size; j++) { const current = this.grid[i][j]; if ( (i > 0 && this.grid[i - 1][j] === current) || (i < this.size - 1 && this.grid[i + 1][j] === current) || (j > 0 && this.grid[i][j - 1] === current) || (j < this.size - 1 && this.grid[i][j + 1] === current) ) { return false; } } } return true; } showGameWon() { const message = document.getElementById('game-message'); message.className = 'game-message game-won'; message.style.display = 'flex'; message.querySelector('p').textContent = '你赢了!'; // 胜利也尝试加币(异步,不阻塞UI) this.tryAwardCoinsOnGameOver(); // 显示最终统计 setTimeout(() => { if (window.gameStats) { window.gameStats.showFinalStats(); } }, 1000); } showGameOver() { const message = document.getElementById('game-message'); message.className = 'game-message game-over'; message.style.display = 'flex'; message.querySelector('p').textContent = '游戏结束!'; // 渲染排行榜 try { this.renderLeaderboard(); } catch (e) { console.error('渲染排行榜时发生错误:', e); } // 尝试加币(异步,不阻塞UI) this.tryAwardCoinsOnGameOver(); // 显示最终统计 setTimeout(() => { if (window.gameStats) { window.gameStats.showFinalStats(); } }, 1000); } restart() { this.score = 0; this.gameWon = false; this.gameOver = false; this.moved = false; // 重置统计数据 this.stats = { moves: 0, startTime: null, gameTime: 0, maxTile: 2, mergeCount: 0 }; this.initializeGrid(); this.addRandomTile(); this.addRandomTile(); this.updateDisplay(); // 隐藏游戏消息 document.getElementById('game-message').style.display = 'none'; // 重新开始计时 this.startTimer(); } startTimer() { this.stats.startTime = Date.now(); if (this.timerInterval) { clearInterval(this.timerInterval); } this.timerInterval = setInterval(() => { if (!this.gameOver && this.stats.startTime) { this.stats.gameTime = Math.floor((Date.now() - this.stats.startTime) / 1000); if (window.gameStats) { window.gameStats.updateDisplay(); } } }, 1000); } // 构建并渲染排行榜 renderLeaderboard() { const container = document.getElementById('leaderboard'); if (!container) return; // 生成当前玩家数据 const today = this.formatDate(new Date()); const currentPlayer = { "名称": "我", "账号": "guest-local", "分数": this.score, "时间": today, _current: true }; // 合并并排序数据(分数由高到低) const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : []; const merged = [...baseData.map(d => ({...d})), currentPlayer] .sort((a, b) => (b["分数"] || 0) - (a["分数"] || 0)); // 计算当前玩家排名 const currentIndex = merged.findIndex(d => d._current); const rank = currentIndex >= 0 ? currentIndex + 1 : '-'; // 仅展示前10条 const topN = merged.slice(0, 10); // 生成 HTML const summaryHtml = `
本局分数:${this.score} 用时:${this.stats.gameTime} 你的排名:${rank}
`; const headerHtml = `
排名
名称
分数
日期
`; const rowsHtml = topN.map((d, i) => { const isCurrent = !!d._current; const rowClass = `leaderboard-row${isCurrent ? ' current' : ''}`; return `
${i + 1}
${this.escapeHtml(d["名称"] || '未知')}
${d["分数"] ?? 0}
${this.escapeHtml(d["时间"] || '-')}
`; }).join(''); container.innerHTML = `
排行榜
${summaryHtml}
${headerHtml}
${rowsHtml}
`; } // 工具:日期格式化 YYYY-MM-DD formatDate(date) { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${y}-${m}-${d}`; } // 工具:简单转义以避免 XSS escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/\"/g, '"') .replace(/'/g, '''); } bindEvents() { // 重试按钮 document.getElementById('retry-btn').addEventListener('click', () => { this.restart(); }); } } // 游戏实例 let game; // 页面加载完成后初始化游戏 document.addEventListener('DOMContentLoaded', () => { game = new Game2048(); // 导出游戏实例供其他模块使用 window.game2048 = game; });