update: 2026-03-28 20:59

This commit is contained in:
2026-03-28 20:59:52 +08:00
parent e21d58e603
commit 1c81d4e6ea
611 changed files with 27847 additions and 65061 deletions

View File

@@ -1,152 +1,22 @@
// 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.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++) {
@@ -156,468 +26,216 @@ class Game2048 {
}
}
}
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 (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 cell = 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);
this.grid[cell.x][cell.y] = value;
this.createTileElement(cell.x, cell.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.style.left = `${y * 25}%`;
tile.style.top = `${x * 25}%`;
tile.dataset.x = x;
tile.dataset.y = y;
tile.dataset.value = value;
container.appendChild(tile);
// 移除动画类
setTimeout(() => {
tile.classList.remove('tile-new');
}, 200);
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]);
}
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;
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();
this.showEndScreen('你赢了!🎉');
} else if (this.isGameOver()) {
this.gameOver = true;
this.showGameOver();
this.showEndScreen('游戏结束!');
}
}
}
moveLeft() {
for (let i = 0; i < this.size; i++) {
const row = this.grid[i].filter(val => val !== 0);
const row = this.grid[i].filter(v => v !== 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++;
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;
row[j + 1] = 0; merged[j] = true;
}
}
const newRow = row.filter(val => val !== 0);
while (newRow.length < this.size) {
newRow.push(0);
}
const newRow = row.filter(v => v !== 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;
}
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 row = this.grid[i].filter(v => v !== 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++;
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;
row[j - 1] = 0; merged[j] = true;
}
}
const newRow = row.filter(val => val !== 0);
while (newRow.length < this.size) {
newRow.unshift(0);
}
const newRow = row.filter(v => v !== 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;
}
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 col = [];
for (let i = 0; i < this.size; i++) { if (this.grid[i][j] !== 0) col.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;
for (let i = 0; i < col.length - 1; i++) {
if (col[i] === col[i + 1] && !merged[i] && !merged[i + 1]) {
col[i] *= 2; this.score += col[i]; this.stats.mergeCount++;
this.stats.maxTile = Math.max(this.stats.maxTile, col[i]);
col[i + 1] = 0; merged[i] = true;
}
}
const newColumn = column.filter(val => val !== 0);
while (newColumn.length < this.size) {
newColumn.push(0);
}
const newCol = col.filter(v => v !== 0);
while (newCol.length < this.size) newCol.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];
if (this.grid[i][j] !== newCol[i]) this.moved = true;
this.grid[i][j] = newCol[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 col = [];
for (let i = 0; i < this.size; i++) { if (this.grid[i][j] !== 0) col.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;
for (let i = col.length - 1; i > 0; i--) {
if (col[i] === col[i - 1] && !merged[i] && !merged[i - 1]) {
col[i] *= 2; this.score += col[i]; this.stats.mergeCount++;
this.stats.maxTile = Math.max(this.stats.maxTile, col[i]);
col[i - 1] = 0; merged[i] = true;
}
}
const newColumn = column.filter(val => val !== 0);
while (newColumn.length < this.size) {
newColumn.unshift(0);
}
const newCol = col.filter(v => v !== 0);
while (newCol.length < this.size) newCol.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];
if (this.grid[i][j] !== newCol[i]) this.moved = true;
this.grid[i][j] = newCol[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;
}
}
}
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 i = 0; i < this.size; i++)
for (let j = 0; j < this.size; j++) {
if (this.grid[i][j] === 0) {
if (this.grid[i][j] === 0) return false;
const c = this.grid[i][j];
if ((i > 0 && this.grid[i-1][j] === c) || (i < this.size-1 && this.grid[i+1][j] === c) ||
(j > 0 && this.grid[i][j-1] === c) || (j < this.size-1 && this.grid[i][j+1] === c))
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() {
showEndScreen(text) {
const message = document.getElementById('game-message');
message.className = 'game-message game-won';
message.className = 'game-message ' + (this.gameWon ? 'game-won' : 'game-over');
message.style.display = 'flex';
message.querySelector('p').textContent = '你赢了!';
// 胜利也尝试加币异步不阻塞UI
this.tryAwardCoinsOnGameOver();
// 显示最终统计
setTimeout(() => {
if (window.gameStats) {
window.gameStats.showFinalStats();
}
}, 1000);
message.querySelector('p').innerHTML =
`${text}<br><span style="font-size:16px;opacity:0.8;">` +
`得分 ${this.score} · 步数 ${this.stats.moves} · ` +
`最大方块 ${this.stats.maxTile} · 用时 ${this.stats.gameTime}秒</span>`;
}
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.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);
}
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 = `
<div class="leaderboard-summary">
<span>本局分数:<strong>${this.score}</strong></span>
<span>用时:<strong>${this.stats.gameTime}</strong> 秒</span>
<span>你的排名:<strong>${rank}</strong></span>
</div>
`;
const headerHtml = `
<div class="leaderboard-header">
<div class="leaderboard-col rank">排名</div>
<div class="leaderboard-col name">名称</div>
<div class="leaderboard-col score">分数</div>
<div class="leaderboard-col time">日期</div>
</div>
`;
const rowsHtml = topN.map((d, i) => {
const isCurrent = !!d._current;
const rowClass = `leaderboard-row${isCurrent ? ' current' : ''}`;
return `
<div class="${rowClass}">
<div class="leaderboard-col rank">${i + 1}</div>
<div class="leaderboard-col name">${this.escapeHtml(d["名称"] || '未知')}</div>
<div class="leaderboard-col score">${d["分数"] ?? 0}</div>
<div class="leaderboard-col time">${this.escapeHtml(d["时间"] || '-')}</div>
</div>
`;
}).join('');
container.innerHTML = `
<div class="leaderboard-title">排行榜</div>
${summaryHtml}
<div class="leaderboard-table">
${headerHtml}
<div class="leaderboard-body">${rowsHtml}</div>
</div>
`;
}
// 工具:日期格式化 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\"/g, '&quot;')
.replace(/'/g, '&#039;');
}
bindEvents() {
// 重试按钮
document.getElementById('retry-btn').addEventListener('click', () => {
this.restart();
});
document.getElementById('retry-btn').addEventListener('click', () => this.restart());
}
}
// 游戏实例
let game;
// 页面加载完成后初始化游戏
document.addEventListener('DOMContentLoaded', () => {
game = new Game2048();
// 导出游戏实例供其他模块使用
window.game2048 = game;
});
});